diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..04f6af5eea --- /dev/null +++ b/.editorconfig @@ -0,0 +1,37 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +# editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false # Trailing whitespace is significant in markdown files + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.rb] +indent_style = space +indent_size = 2 + +[fastlane/*] +indent_style = space +indent_size = 2 + +[Makefile] +indent_style = tab +indent_size = 4 + +[*.{json,xcstrings}] +indent_size = 4 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000..6deafc2617 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 diff --git a/.gitattributes b/.gitattributes index 0e8cfa4c2c..bac95c5043 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +1,12 @@ -*.mp3 filter=lfs diff=lfs merge=lfs -text *.bin filter=lfs diff=lfs merge=lfs -text -*.png filter=lfs diff=lfs merge=lfs -text -*.svg filter=lfs diff=lfs merge=lfs -text -*.jpg filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text *.jpeg filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.mid filter=lfs diff=lfs merge=lfs -text +*.mp3 filter=lfs diff=lfs merge=lfs -text *.pdf filter=lfs diff=lfs merge=lfs -text -*.gif filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.svg filter=lfs diff=lfs merge=lfs -text +*.wav filter=lfs diff=lfs merge=lfs -text +*.json merge=json +*.xcstrings merge=json diff --git a/.github/workflows/ci-fastlane-release_app_store.yml b/.github/workflows/ci-fastlane-release_app_store.yml new file mode 100644 index 0000000000..4cf6ebfc80 --- /dev/null +++ b/.github/workflows/ci-fastlane-release_app_store.yml @@ -0,0 +1,104 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +name: Fastlane - Release to App Store + +on: + pull_request: + types: [labeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + TUIST_TURN_OFF_LINTERS: TRUE + APP_STORE_CONNECT_API_KEY_CONTENT_RELEASE_APP_STORE: ${{ secrets. APP_STORE_CONNECT_API_KEY_CONTENT_RELEASE_APP_STORE }} + APP_STORE_CONNECT_ISSUER_ID: ${{ secrets. APP_STORE_CONNECT_ISSUER_ID }} + APP_STORE_CONNECT_API_KEY_ID_RELEASE_APP_STORE: ${{ secrets. APP_STORE_CONNECT_API_KEY_ID_RELEASE_APP_STORE }} + FASTLANE_KEYCHAIN_PASSWORD: ${{ secrets.FASTLANE_KEYCHAIN_PASSWORD }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} + +jobs: + release_app_store: + if: contains(github.event.label.name, 'fastlane:deliver') + name: fastlane upload_to_app_store + runs-on: [self-hosted, iOS] + + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + FASTLANE_SKIP_UPDATE_CHECK: 1 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 # shallow clone + + - name: Setup mise + run: | + echo "$HOME/.local/share/mise/shims" >> $GITHUB_PATH + + - name: bundle install + run: | + bundle install + + - name: tuist fetch + run: | + tuist fetch + + - name: fastlane helloworld + run: | + bundle exec fastlane helloworld + + - name: fastlane sync_certificates + run: | + bundle exec fastlane sync_certificates release:true --verbose + + - name: fastlane release LekaApp + if: contains(github.event.label.name, 'LekaApp') + run: | + bundle exec fastlane release target:LekaApp --verbose + + - name: fastlane release LekaUpdater + if: contains(github.event.label.name, 'LekaUpdater') + run: | + bundle exec fastlane release target:LekaUpdater --verbose + + - name: post message + if: ${{ success() }} + uses: marocchino/sticky-pull-request-comment@v2 + with: + hide_and_recreate: true + hide_classify: "OUTDATED" + header: release + message: | + ## New AppStore Release available :rocket: + + @leka/dev-ios + + A new version has been uploaded to the App Store and is ready for review. + + | | | + |-------------|---------------------------------| + | **App** | `${{ env.APP_NAME }}` | + | **Version** | `${{ env.APP_VERSION_NUMBER }}` | + | **Build** | `${{ env.APP_BUILD_NUMBER }}` | + + - name: remove label + if: always() + uses: actions/github-script@v6 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const { LABEL_NAME } = process.env + github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: LABEL_NAME + }) + env: + LABEL_NAME: ${{ github.event.label.name }} diff --git a/.github/workflows/ci-fastlane-release_beta_internal.yml b/.github/workflows/ci-fastlane-release_beta_internal.yml new file mode 100644 index 0000000000..d6fa73dd4c --- /dev/null +++ b/.github/workflows/ci-fastlane-release_beta_internal.yml @@ -0,0 +1,122 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +name: Fastlane - Internal Beta Release + +on: + pull_request: + types: [labeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + TUIST_TURN_OFF_LINTERS: TRUE + APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets. APP_STORE_CONNECT_API_KEY_CONTENT }} + APP_STORE_CONNECT_ISSUER_ID: ${{ secrets. APP_STORE_CONNECT_ISSUER_ID }} + APP_STORE_CONNECT_API_KEY_ID: ${{ secrets. APP_STORE_CONNECT_API_KEY_ID }} + FASTLANE_KEYCHAIN_PASSWORD: ${{ secrets.FASTLANE_KEYCHAIN_PASSWORD }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} + +jobs: + internal_beta_release: + if: contains(github.event.label.name, 'fastlane:beta') + name: fastlane release beta_internal + runs-on: [self-hosted, iOS] + + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + FASTLANE_SKIP_UPDATE_CHECK: 1 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: true + + - name: Checkout base, head branches + run: | + git checkout ${{ env.BASE_REF }} + git checkout ${{ env.HEAD_REF }} + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + HEAD_REF: ${{ github.head_ref }} + + - name: Setup mise + run: | + echo "$HOME/.local/share/mise/shims" >> $GITHUB_PATH + + - name: bundle install + run: | + bundle install + + - name: tuist fetch + run: | + tuist fetch + + - name: fastlane helloworld + run: | + bundle exec fastlane helloworld + + - name: fastlane sync_certificates + run: | + bundle exec fastlane sync_certificates release:true --verbose + + - name: fastlane beta_internal LekaApp + if: contains(github.event.label.name, 'LekaApp') + run: | + bundle exec fastlane beta_internal targets:LekaApp --verbose + + - name: fastlane beta_internal LekaActivityUIExplorer + if: contains(github.event.label.name, 'LekaActivityUIExplorer') + run: | + bundle exec fastlane beta_internal targets:LekaActivityUIExplorer --verbose + + - name: fastlane beta_internal LekaUpdater + if: contains(github.event.label.name, 'LekaUpdater') + run: | + bundle exec fastlane beta_internal targets:LekaUpdater --verbose + + - name: fastlane beta_internal all + if: contains(github.event.label.name, 'all') + run: | + bundle exec fastlane beta_internal targets:all --verbose + + - name: Post Github comment + if: ${{ success() }} + uses: marocchino/sticky-pull-request-comment@v2 + with: + hide_and_recreate: true + hide_classify: "OUTDATED" + header: testflight + message: | + ${{ env.CHANGELOG_FOR_GITHUB }} + + - name: Post to a Slack channel + id: slack + uses: slackapi/slack-github-action@v1.25.0 + with: + channel-id: "C3SHVTYNP,C041YEWNVJS" + slack-message: ${{ env.CHANGELOG_FOR_SLACK }} + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + + - name: remove label + if: always() + uses: actions/github-script@v6 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const { LABEL_NAME } = process.env + github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: LABEL_NAME + }) + env: + LABEL_NAME: ${{ github.event.label.name }} diff --git a/.github/workflows/ci-linter-license_checker.yml b/.github/workflows/ci-linter-license_checker.yml new file mode 100644 index 0000000000..cb8d288ceb --- /dev/null +++ b/.github/workflows/ci-linter-license_checker.yml @@ -0,0 +1,34 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +name: Linter - License Checker + +on: + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + license_checker: + name: lint + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + lfs: true + + - name: Install deno + uses: denoland/setup-deno@v1 + with: + deno-version: vx.x.x + + - name: Check licenses in all files + run: | + deno run --allow-read https://deno.land/x/license_checker@v3.2.2/main.ts diff --git a/.github/workflows/ci-linter-pre_commit_hooks.yml b/.github/workflows/ci-linter-pre_commit_hooks.yml new file mode 100644 index 0000000000..82ae1ae0d6 --- /dev/null +++ b/.github/workflows/ci-linter-pre_commit_hooks.yml @@ -0,0 +1,29 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +name: Linter - pre-commit hooks + +on: + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + swift_format: + name: pre-commit hooks + runs-on: [self-hosted, iOS] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 # Fetch all history for all branches and tags + lfs: true + + - name: Run pre-commit hooks --all-files + run: | + pre-commit run --show-diff-on-failure --color=always --all-files diff --git a/.github/workflows/ci-linter-swiftformat.yml b/.github/workflows/ci-linter-swiftformat.yml new file mode 100644 index 0000000000..1c26138e80 --- /dev/null +++ b/.github/workflows/ci-linter-swiftformat.yml @@ -0,0 +1,39 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +name: Linter - SwiftFormat + +on: + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + swift_format: + name: swiftformat + runs-on: [self-hosted, iOS] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for all branches and tags + lfs: true + + - name: Setup mise + run: | + echo "$HOME/.local/share/mise/shims" >> $GITHUB_PATH + + - name: Run swiftformat + run: | + echo "" + echo "🏃‍♂️ Running swiftformat" + + which swiftformat + swiftformat --version + + swiftformat --lint --reporter github-actions-log . diff --git a/.github/workflows/ci-linter-swiftlint.yml b/.github/workflows/ci-linter-swiftlint.yml new file mode 100644 index 0000000000..54a9f26748 --- /dev/null +++ b/.github/workflows/ci-linter-swiftlint.yml @@ -0,0 +1,45 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +name: Linter - SwiftLint + +on: + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + swift_format: + name: swiftlint + runs-on: [self-hosted, iOS] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for all branches and tags + lfs: true + + - name: Setup mise + run: | + echo "$HOME/.local/share/mise/shims" >> $GITHUB_PATH + + - name: Run swiftlint + run: | + echo "" + echo "🏃‍♂️ Running swiftlint on modified files 🤵‍♂️" + + which swiftlint + swiftlint --version + + git diff origin/main --name-only --diff-filter=AMCR \ + | grep -E "\.swift\$$" \ + || echo "No files added nor modified!" + + git diff origin/main --name-only --diff-filter=AMCR \ + | grep -E "\.swift\$$" \ + | xargs --no-run-if-empty swiftlint lint --quiet --reporter github-actions-logging diff --git a/.github/workflows/ci-system-upgrade_tools_and_clean.yml b/.github/workflows/ci-system-upgrade_tools_and_clean.yml new file mode 100644 index 0000000000..9212853cb1 --- /dev/null +++ b/.github/workflows/ci-system-upgrade_tools_and_clean.yml @@ -0,0 +1,119 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +name: System - Upgrade tools & clean + +on: + pull_request: + types: [labeled] + schedule: + # Runs at 3:00 UTC every day + - cron: "0 3 * * *" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + upgrade_tools: + if: | + (github.event_name == 'pull_request' && github.event.label.name == 'ci:upgrade_tools') || + contains(github.event_name, 'schedule') + name: Upgrade tools + runs-on: [self-hosted, iOS] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 # shallow clone + + - name: Setup mise + run: | + echo "$HOME/.local/share/mise/shims" >> $GITHUB_PATH + + - name: Tools' versions before + run: | + which mise + mise --version + + which xcodebuild + xcodebuild -version + + which tuist + tuist version + + which fastlane + fastlane --version + + which swiftlint + swiftlint --version + + which swiftformat + swiftformat --version + + which bundle + bundle --version + + - name: Upgrade brew + run: | + brew update + brew upgrade + brew cleanup -s --prune=all + + - name: Install mise tools + run: | + mise uninstall --all + mise install + + - name: Upgrade bundle + run: | + bundle update --bundler + + - name: tuist clean + run: | + tuist clean + + - name: Delete Xcode derived data + run: | + rm -rf ~/Library/Developer/Xcode/DerivedData + + - name: Tools' versions after + run: | + which mise + mise --version + + which xcodebuild + xcodebuild -version + + which tuist + tuist version + + which fastlane + fastlane --version + + which swiftlint + swiftlint --version + + which swiftformat + swiftformat --version + + which bundle + bundle --version + + - name: Remove label + if: github.event_name == 'pull_request' && github.event.label.name == 'ci:upgrade_tools' + uses: actions/github-script@v6 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const { LABEL_NAME } = process.env + github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: LABEL_NAME + }) + env: + LABEL_NAME: ${{ github.event.label.name }} diff --git a/.github/workflows/ci-tuist-build.yml b/.github/workflows/ci-tuist-build.yml new file mode 100644 index 0000000000..cc7856aa94 --- /dev/null +++ b/.github/workflows/ci-tuist-build.yml @@ -0,0 +1,47 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +name: Tuist - Build + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + TUIST_TURN_OFF_LINTERS: TRUE + +jobs: + build: + name: build + runs-on: [self-hosted, iOS] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 # shallow clone + lfs: true + + - name: Setup mise + run: | + echo "$HOME/.local/share/mise/shims" >> $GITHUB_PATH + + - name: tuist fetch + run: | + TUIST_TURN_OFF_LINTERS=TRUE tuist fetch + + - name: tuist generate + run: | + TUIST_TURN_OFF_LINTERS=TRUE tuist generate -n + + - name: tuist build + run: | + tuist build diff --git a/.github/workflows/ci-tuist-unit_tests.yml b/.github/workflows/ci-tuist-unit_tests.yml new file mode 100644 index 0000000000..afeb06d730 --- /dev/null +++ b/.github/workflows/ci-tuist-unit_tests.yml @@ -0,0 +1,47 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +name: Tuist - Unit Tests + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + TUIST_TURN_OFF_LINTERS: TRUE + +jobs: + unit_tests: + name: unit tests + runs-on: [self-hosted, iOS] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 # shallow clone + lfs: true + + - name: Setup mise + run: | + echo "$HOME/.local/share/mise/shims" >> $GITHUB_PATH + + - name: tuist fetch + run: | + tuist fetch + + - name: tuist generate + run: | + tuist generate -n + + - name: tuist test + run: | + tuist test diff --git a/.gitignore b/.gitignore index 4faad1346e..9fcac358b5 100644 --- a/.gitignore +++ b/.gitignore @@ -65,7 +65,26 @@ DerivedData/ ### Tuist derived files ### graph.dot graph.png +graph.json Derived/ +.build ### Tuist managed dependencies ### Tuist/Dependencies + +### Tuist signing - siging is managed by fastlane +Tuist/Signing + +### Fastlane + +# fastlane specific +**/fastlane/report.xml + +# deliver temporary files +**/fastlane/Preview.html + +# snapshot generated screenshots +**/fastlane/screenshots + +# scan temporary files +**/fastlane/test_output diff --git a/.gitmojirc.json b/.gitmojirc.json new file mode 100644 index 0000000000..954bf5d82e --- /dev/null +++ b/.gitmojirc.json @@ -0,0 +1,8 @@ +{ + "autoAdd": false, + "capitalizeTitle": true, + "emojiFormat": "emoji", + "gitmojisUrl": "https://gitmoji.dev/api/gitmojis", + "messagePrompt": true, + "scopePrompt": true +} diff --git a/.licenserc.json b/.licenserc.json new file mode 100644 index 0000000000..81f65b8a86 --- /dev/null +++ b/.licenserc.json @@ -0,0 +1,28 @@ +{ + "**/*.swift": [ + "// Leka - iOS Monorepo", + "// Copyright APF France handicap", + "// SPDX-License-Identifier: Apache-2.0\n" + ], + "**/*.yml": [ + "# Leka - iOS Monorepo", + "# Copyright APF France handicap", + "# SPDX-License-Identifier: Apache-2.0\n" + ], + ".github/**/*.yml": [ + "# Leka - iOS Monorepo", + "# Copyright APF France handicap", + "# SPDX-License-Identifier: Apache-2.0\n" + ], + "Gemfile": [ + "# Leka - iOS Monorepo", + "# Copyright APF France handicap", + "# SPDX-License-Identifier: Apache-2.0\n" + ], + "ignore": [ + "Dependencies/", + "Derived/", + ".build/", + "Apps/LekaApp/Resources/Media/" + ] +} diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000000..d143d3e7d3 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,4 @@ +[tools] +tuist = "3.42.2" +swiftlint = "0.53.0" +swiftformat = "0.53.1" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..bc63e8e25c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,123 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +# ? See https://pre-commit.com for more information +# ? See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + exclude_types: [json] + exclude: '(.*\.xcstrings|\.xcassets)' + - id: check-yaml + exclude: exercise_templates.yml + - id: check-json + types: [file] + files: \.(json|xcstrings)$ + exclude: '(.*\.xcassets/.*\.json$)' + - id: pretty-format-json + args: ["--autofix", "--indent=4", "--top-keys=version,sourceLanguage"] + types: [file] + files: \.(json|xcstrings)$ + exclude: '(\.vscode/settings\.json|\.jtd\.json$|.*\.xcassets/.*|.*\.colorset/.*|\.animation\.lottie\.json$)' + - id: check-added-large-files + - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable + - id: detect-private-key + - id: forbid-submodules + - id: mixed-line-ending + + - repo: https://github.com/realm/SwiftLint + rev: 0.54.0 + hooks: + - id: swiftlint + entry: swiftlint + + - repo: https://github.com/nicklockwood/SwiftFormat + rev: 0.53.1 + hooks: + - id: swiftformat + + - repo: local + hooks: + - id: check_xcstrings + name: Check .xcstrings files for stale entries and unusual characters + description: This hook checks .xcstrings files for stale entries and unusual characters + entry: python3 Tools/Hooks/check_xcstrings.py + language: python + additional_dependencies: ["pygments"] + files: '.*\.xcstrings' + + - id: check_yaml_definitions_avatars + name: Check avatars.yml + description: | + This hook checks avatars.yml for: + - non unique ids + - jtd schema validation + - filename and image consistency + It also formats the file + entry: python3 Tools/Hooks/check_yaml_definitions_avatars.py + language: python + additional_dependencies: ["ruamel.yaml"] + files: avatars.yml + + - id: check_yaml_definitions_professions + name: Check professions.yml + description: | + This hook checks professions.yml for: + - non unique ids + - jtd schema validation + It also formats the file and sorts the entries + entry: python3 Tools/Hooks/check_yaml_definitions_professions.py + language: python + additional_dependencies: ["ruamel.yaml"] + files: professions.yml + + - id: check_yaml_definitions_authors + name: Check authors.yml + description: | + This hook checks authors.yml for: + - non unique ids + - jtd schema validation + It also formats the file and sorts the entries + entry: python3 Tools/Hooks/check_yaml_definitions_authors.py + language: python + additional_dependencies: ["ruamel.yaml"] + files: authors.yml + + - id: check_yaml_definitions_skills + name: Check skills.yml + description: | + This hook checks skills.yml for: + - non unique ids + - jtd schema validation + It also formats the file and sorts the entries + entry: python3 Tools/Hooks/check_yaml_definitions_skills.py + language: python + additional_dependencies: ["ruamel.yaml"] + files: skills.yml + + - id: check_yaml_content_activities + name: Check activity.yml files + description: | + This hook checks activity.yml files for: + - uuid and filename consistency + - jtd schema validation + entry: python3 Tools/Hooks/check_yaml_content_activities.py + language: python + additional_dependencies: ["ruamel.yaml"] + files: .*\.activity\.yml + types: [yaml] + + - id: check_yaml_content_activities_unique_uuid + name: Check activity.yml files for unique uuid + description: | + This hook checks activity.yml files for unique uuid + entry: python3 Tools/Hooks/check_yaml_content_activities_unique_uuid.py + language: python + files: .*\.activity\.yml + types: [yaml] + pass_filenames: false diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000000..84b0bf7a54 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,9 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +AllCops: + NewCops: enable + +Metrics/BlockLength: + Enabled: false diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 0000000000..32818cb300 --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1,10 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +sonar.cpd.exclusions = \ + Tuist/**/*, \ + **/Project.swift, \ + Tools/**/*, \ + **/*OLDCodeBase/**/*, \ + **/*DEPRECATED.swift, \ diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000000..c4c92232b6 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,172 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +--disable all +--swiftversion 5.9 + +# Enabled rules + +--enable acronyms +--enable andOperator +--enable anyObjectProtocol +--enable applicationMain +--enable assertionFailures +--enable blankLineAfterImports + +--enable blankLinesAroundMark +--lineaftermarks true + +--enable blankLinesAtEndOfScope +--enable blankLinesAtStartOfScope +--typeblanklines remove + +--enable blankLinesBetweenChainedFunctions +--enable blankLinesBetweenScopes + +--enable blockComments + +--enable braces +--allman false + +--enable conditionalAssignment + +--enable consecutiveBlankLines +--enable consecutiveSpaces + +--enable duplicateImports + +--enable elseOnSameLine +--elseposition same-line +--guardelse auto + +--enable emptyBraces +--emptybraces no-space + +--enable enumNamespaces +--enumnamespaces always + +--enable extensionAccessControl +--extensionacl on-extension + +--enable fileHeader +--header Leka - iOS Monorepo\nCopyright APF France handicap\nSPDX-License-Identifier: Apache-2.0 + +--enable genericExtensions + +--enable headerFileName + +--enable hoistAwait + +--enable hoistPatternLet +--patternlet hoist + +--enable hoistTry + +--enable indent +--indent 4 +--indentcase true +--indentstrings true + +--enable initCoderUnavailable + +--enable isEmpty + +--enable leadingDelimiters + +--enable linebreakAtEndOfFile + +--enable markTypes + +--enable modifierOrder + +--enable numberFormatting +--hexliteralcase uppercase +--exponentcase lowercase + +--enable opaqueGenericParameters + +--enable organizeDeclarations + +--enable preferKeyPath + +--enable redundantBackticks +--enable redundantBreak +--enable redundantClosure +--enable redundantExtensionACL +--enable redundantFileprivate +--enable redundantGet +--enable redundantInit +--enable redundantInternal +--enable redundantLet +--enable redundantLetError +--enable redundantNilInit +--enable redundantObjc +--enable redundantOptionalBinding +--enable redundantParens +--enable redundantPattern +--enable redundantRawValues +--enable redundantReturn + +--enable redundantSelf +--self insert + +--enable redundantStaticSelf +--enable redundantType +--enable redundantVoidReturnType + +--enable semicolons +--enable sortDeclarations + +--enable sortImports +--importgrouping testable-bottom + +--enable spaceAroundBraces +--enable spaceAroundBrackets +--enable spaceAroundComments +--enable spaceAroundGenerics + +--enable spaceAroundOperators +--ranges no-space + +--enable spaceAroundParens +--enable spaceInsideBraces +--enable spaceInsideBrackets +--enable spaceInsideComments +--enable spaceInsideGenerics +--enable spaceInsideParens + +--enable strongifiedSelf + +--enable todos + +--enable trailingClosures +--enable trailingCommas +--enable trailingSpace + +--enable typeSugar + +--enable unusedArguments + +--enable void + +--enable wrap + +--enable wrapArguments +--wraparguments preserve +--wrapcollections preserve +--wrapconditions after-first + +--enable wrapAttributes +--enable wrapEnumCases +--enable wrapMultilineStatementBraces +--enable wrapSingleLineComments +--enable wrapSwitchCases + +--enable yodaConditions + +# Disabled rules + +--disable blankLinesBetweenImports +--disable docComments +--disable sortSwitchCases diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000000..bb6e58aac1 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,68 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +opt_in_rules: [] + +trailing_comma: + mandatory_comma: true + +type_name: + min_length: 4 # only warning + max_length: # warning and error + warning: 40 + error: 50 + allowed_symbols: ["_"] # these are allowed in type names + excluded: + - i18n + - l10n + +identifier_name: + min_length: # only min_length + error: 4 # only error + excluded: + - a + - app + - args + - b + - ble + - BLE + - cmd + - g + - hmi + - HMI + - hmis + - HMIs + - i + - i18n + - id + - img + - key + - l10n + - lhs + - log + - new + - r + - red + - rgb + - rhs + - rx + - tx + - URL + +line_length: + warning: 150 + ignores_urls: true + ignores_function_declarations: true + ignores_interpolated_strings: true + +disabled_rules: + - opening_brace + - switch_case_alignment + - closure_parameter_position + +excluded: + - Tuist/Dependencies + - ./*/Derived + +reporter: "emoji" diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..53b6ee872e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "files.associations": { + "*.xcstrings": "json" + }, + "[xcstrings]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/Apps/BLEKitExample/Project.swift b/Apps/BLEKitExample/Project.swift new file mode 100644 index 0000000000..07b005436b --- /dev/null +++ b/Apps/BLEKitExample/Project.swift @@ -0,0 +1,16 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.app( + name: "BLEKitExample", + dependencies: [ + .project(target: "DesignKit", path: Path("../../Modules/DesignKit")), + .project(target: "BLEKit", path: Path("../../Modules/BLEKit")), + ] +) diff --git a/Apps/LekaEmotions/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Apps/BLEKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Apps/LekaEmotions/Resources/Assets.xcassets/AccentColor.colorset/Contents.json rename to Apps/BLEKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Apps/LekaEmotions/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Apps/BLEKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Apps/LekaEmotions/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Apps/BLEKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Apps/LekaEmotions/Resources/Assets.xcassets/Contents.json b/Apps/BLEKitExample/Resources/Assets.xcassets/Contents.json similarity index 100% rename from Apps/LekaEmotions/Resources/Assets.xcassets/Contents.json rename to Apps/BLEKitExample/Resources/Assets.xcassets/Contents.json diff --git a/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-1-green-spin.imageset/Contents.json b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-1-green-spin.imageset/Contents.json new file mode 100644 index 0000000000..15e12a8831 --- /dev/null +++ b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-1-green-spin.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "reinforcer-1-green-spin.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-1-green-spin.imageset/reinforcer-1-green-spin.svg b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-1-green-spin.imageset/reinforcer-1-green-spin.svg new file mode 100644 index 0000000000..7e260d50da --- /dev/null +++ b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-1-green-spin.imageset/reinforcer-1-green-spin.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0fa5f6ec28b5f7f8727afed5922766ca4f033747cec6e0c34694a21eeb95cf08 +size 4946 diff --git a/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-2-violet_green_blink-spin.imageset/Contents.json b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-2-violet_green_blink-spin.imageset/Contents.json new file mode 100644 index 0000000000..0279507d39 --- /dev/null +++ b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-2-violet_green_blink-spin.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "reinforcer-2-violet_green_blink-spin.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-2-violet_green_blink-spin.imageset/reinforcer-2-violet_green_blink-spin.svg b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-2-violet_green_blink-spin.imageset/reinforcer-2-violet_green_blink-spin.svg new file mode 100644 index 0000000000..50ce37d59b --- /dev/null +++ b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-2-violet_green_blink-spin.imageset/reinforcer-2-violet_green_blink-spin.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9424e2f561077422968f7968143cc53ad900fd9019a469fe24d92759438c5e60 +size 4933 diff --git a/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-3-fire-static.imageset/Contents.json b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-3-fire-static.imageset/Contents.json new file mode 100644 index 0000000000..aeb8603bbd --- /dev/null +++ b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-3-fire-static.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "reinforcer-3-fire-static.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-3-fire-static.imageset/reinforcer-3-fire-static.svg b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-3-fire-static.imageset/reinforcer-3-fire-static.svg new file mode 100644 index 0000000000..d5bfe6636b --- /dev/null +++ b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-3-fire-static.imageset/reinforcer-3-fire-static.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7293e3337957d1cee5947c4a03784e9f6614c3935b9cdb5ae185416870adb9d2 +size 2016 diff --git a/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-4-glitters-static.imageset/Contents.json b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-4-glitters-static.imageset/Contents.json new file mode 100644 index 0000000000..98b4efb4c3 --- /dev/null +++ b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-4-glitters-static.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "reinforcer-4-glitters-static.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-4-glitters-static.imageset/reinforcer-4-glitters-static.svg b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-4-glitters-static.imageset/reinforcer-4-glitters-static.svg new file mode 100644 index 0000000000..f4c9463d4a --- /dev/null +++ b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-4-glitters-static.imageset/reinforcer-4-glitters-static.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fc67778182ce47cc2dcbe8a985f90f00bebe38281e4eb3f3c599ad4720f90d9 +size 8664 diff --git a/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-5-rainbow-static.imageset/Contents.json b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-5-rainbow-static.imageset/Contents.json new file mode 100644 index 0000000000..15c8b925e9 --- /dev/null +++ b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-5-rainbow-static.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "reinforcer-5-rainbow-static.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-5-rainbow-static.imageset/reinforcer-5-rainbow-static.svg b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-5-rainbow-static.imageset/reinforcer-5-rainbow-static.svg new file mode 100644 index 0000000000..e41df8a1ea --- /dev/null +++ b/Apps/BLEKitExample/Resources/Assets.xcassets/reinforcer-5-rainbow-static.imageset/reinforcer-5-rainbow-static.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12c6a9d65a739cb0dd6fe2ae45fca3d18d951c06d08c381147073a33440591a6 +size 2120 diff --git a/Apps/BLEKitExample/Resources/Assets.xcassets/robot_connexion_bluetooth.imageset/Contents.json b/Apps/BLEKitExample/Resources/Assets.xcassets/robot_connexion_bluetooth.imageset/Contents.json new file mode 100644 index 0000000000..3123cb756d --- /dev/null +++ b/Apps/BLEKitExample/Resources/Assets.xcassets/robot_connexion_bluetooth.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "robot_connexion_bluetooth.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/BLEKitExample/Resources/Assets.xcassets/robot_connexion_bluetooth.imageset/robot_connexion_bluetooth.svg b/Apps/BLEKitExample/Resources/Assets.xcassets/robot_connexion_bluetooth.imageset/robot_connexion_bluetooth.svg new file mode 100644 index 0000000000..9ae5e10feb --- /dev/null +++ b/Apps/BLEKitExample/Resources/Assets.xcassets/robot_connexion_bluetooth.imageset/robot_connexion_bluetooth.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be6c0dbe42f4ad788b955040166c851d448754de7d5075fd3b641d4384741518 +size 691 diff --git a/Apps/LekaEmotions/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Apps/BLEKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json similarity index 100% rename from Apps/LekaEmotions/Resources/Preview Content/Preview Assets.xcassets/Contents.json rename to Apps/BLEKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/Apps/BLEKitExample/Sources/Models/Commands.swift b/Apps/BLEKitExample/Sources/Models/Commands.swift new file mode 100644 index 0000000000..621db54524 --- /dev/null +++ b/Apps/BLEKitExample/Sources/Models/Commands.swift @@ -0,0 +1,158 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +// MARK: - LKCommand + +enum LKCommand { + // MARK: - LKCommand Led Full + + enum LedFull { + static let command: UInt8 = 0x13 + static let numberOfValues: UInt8 = 1 + 3 + 1 // EAR/BELT + R, G, B + Checksum + static let ears: UInt8 = 0x14 + static let belt: UInt8 = 0x15 + } + + // MARK: - LKCommand Motors + + enum Motor { + static let command: UInt8 = 0x20 + static let numberOfValues: UInt8 = 1 + 2 + 1 // ID + Spin, Speed + Checksum + static let left: UInt8 = 0x21 + static let right: UInt8 = 0x22 + + static let forward: UInt8 = 0x01 + static let backward: UInt8 = 0x00 + } + + // MARK: - LKCommand Motivator + + enum Motivator { + static let command: UInt8 = 0x50 + static let numberOfValues: UInt8 = 1 + 1 // Motivator + Checksum + static let rainbow: UInt8 = 0x51 + static let fire: UInt8 = 0x52 + static let sprinkles: UInt8 = 0x53 + static let spinBlink: UInt8 = 0x54 + static let blinkGreen: UInt8 = 0x55 + } + + static let startByte: UInt8 = 0x2A + static let startByteLength: UInt8 = 0x04 + static let ledFull: UInt8 = 0x13 + static let motor: UInt8 = 0x20 + static let motivator: UInt8 = 0x50 +} + +func checksum8(_ values: [UInt8]) -> UInt8 { + var checksum = 0 + + for value in values { + checksum = (Int(value) + checksum) % 256 + } + + return UInt8(checksum) +} + +// MARK: - CommandKit + +class CommandKit { + // MARK: Lifecycle + + // MARK: - Initialisation + + init() { + for _ in 0...LKCommand.startByteLength - 1 { + self.startSequence.append(LKCommand.startByte) + } + } + + // MARK: Internal + + // MARK: - Singleton + + static let sharedSingleton = CommandKit() + + let command = LKCommand.self + + // MARK: - Variables + + var startSequence: [UInt8] = [] + var commandSequence: [UInt8] = [] + + var numberOfCommands: Int = 0 + var isEncapsulated: Bool = false + + // MARK: - Led Functions + + func addAllLeds(of earOrBelt: UInt8, rgbColor red: UInt8, _ green: UInt8, _ blue: UInt8) { + let array = [command.LedFull.command, earOrBelt, red, green, blue, checksum8([earOrBelt, red, green, blue])] + + for element in array { + self.commandSequence.append(element) + } + + self.numberOfCommands += 1 + } + + // MARK: - Motor Functions + + func addMotor(on leftOrRight: UInt8, direction: UInt8, speed: UInt8) { + let array = [command.Motor.command, leftOrRight, direction, speed, checksum8([leftOrRight, direction, speed])] + + for element in array { + self.commandSequence.append(element) + } + + self.numberOfCommands += 1 + } + + // MARK: - Motivator Functions + + func addMotivator(_ motivator: UInt8) { + let array = [command.Motivator.command, motivator, checksum8([motivator])] + + for element in array { + self.commandSequence.append(element) + } + + self.numberOfCommands += 1 + } + + // MARK: - Command Functions + + func encapsulate() -> [UInt8] { + var encapsulatedArray: [UInt8] = [] + + encapsulatedArray.append(contentsOf: self.startSequence) + encapsulatedArray.append(UInt8(self.numberOfCommands)) + encapsulatedArray.append(contentsOf: self.commandSequence) + + self.commandSequence = encapsulatedArray + self.isEncapsulated = true + + return self.commandSequence + } + + func getCommands() -> [UInt8] { + var commands: [UInt8] = [] + + if self.isEncapsulated { + commands = self.commandSequence + } else { + commands = self.encapsulate() + } + + self.flush() + return commands + } + + func flush() { + self.commandSequence.removeAll() + self.numberOfCommands = 0 + self.isEncapsulated = false + } +} diff --git a/Apps/BLEKitExample/Sources/Models/Robot.swift b/Apps/BLEKitExample/Sources/Models/Robot.swift new file mode 100644 index 0000000000..86ab4ae13e --- /dev/null +++ b/Apps/BLEKitExample/Sources/Models/Robot.swift @@ -0,0 +1,153 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import Foundation + +class Robot: ObservableObject { + // MARK: Internal + + @Published var manufacturer: String = "" + @Published var modelNumber: String = "" + @Published var serialNumber: String = "" + @Published var osVersion: String = "" + + @Published var battery: Int = 0 + @Published var isCharging: Bool = false + + @Published var magicCardID: Int = 0 + @Published var magicCardLanguage: String = "" + + let commands = CommandKit() + + var robotPeripheral: RobotPeripheral? { + didSet { + self.updateDeviceInformation() + self.subscribeToDeviceUpdates() + } + } + + func updateDeviceInformation() { + guard let robotPeripheral else { return } + + self.registerReadOnlyCharacteristicClosures() + + robotPeripheral.readReadOnlyCharacteristics() + } + + func subscribeToDeviceUpdates() { + guard let robotPeripheral else { return } + + self.registerNotifyingCharacteristicClosures() + + robotPeripheral.discoverAndListenForUpdates() + } + + func runReinforcer(_ reinforcer: UInt8) { + guard let robotPeripheral else { return } + + self.commands.addMotivator(reinforcer) + let data = Data(commands.getCommands()) + + robotPeripheral.sendCommand(data) + } + + // MARK: Private + + private func registerReadOnlyCharacteristicClosures() { + for char in kDefaultReadOnlyCharacteristics { + var newChar = char + + if newChar.characteristicUUID == BLESpecs.DeviceInformation.Characteristics.manufacturer { + newChar.onNotification = { + [weak self] data in + if let data { + self?.manufacturer = String( + decoding: data, as: UTF8.self + ) + } + } + } + + if newChar.characteristicUUID == BLESpecs.DeviceInformation.Characteristics.modelNumber { + newChar.onNotification = { + [weak self] data in + if let data { + self?.modelNumber = String( + decoding: data, as: UTF8.self + ) + } + } + } + + if newChar.characteristicUUID == BLESpecs.DeviceInformation.Characteristics.serialNumber { + newChar.onNotification = { + [weak self] data in + if let data { + self?.serialNumber = String( + decoding: data, as: UTF8.self + ) + } + } + } + + if newChar.characteristicUUID == BLESpecs.DeviceInformation.Characteristics.osVersion { + newChar.onNotification = { + [weak self] data in + if let data { + self?.osVersion = String( + decoding: data, as: UTF8.self + ) + } + } + } + + self.robotPeripheral?.readOnlyCharacteristics.insert(newChar) + } + } + + private func registerNotifyingCharacteristicClosures() { + for char in kDefaultNotifyingCharacteristics { + var newChar = char + + if newChar.characteristicUUID == BLESpecs.Battery.Characteristics.level { + newChar.onNotification = { + [weak self] data in + if let value = data?.first { + self?.battery = Int(value) + } + } + } + + if newChar.characteristicUUID == BLESpecs.Monitoring.Characteristics.chargingStatus { + newChar.onNotification = { + [weak self] data in + if let value = data?.first { + self?.isCharging = (value == 0x01) + } + } + } + + if newChar.characteristicUUID == BLESpecs.MagicCard.Characteristics.id { + newChar.onNotification = { + [weak self] data in + if let data { + self?.magicCardID = Int(data[1]) + } + } + } + + if newChar.characteristicUUID == BLESpecs.MagicCard.Characteristics.language { + newChar.onNotification = { + [weak self] data in + if let value = data?.first { + self?.magicCardLanguage = (value == 0x01 ? "FR" : "EN") + } + } + } + + self.robotPeripheral?.notifyingCharacteristics.insert(newChar) + } + } +} diff --git a/Apps/BLEKitExample/Sources/Stores/BotViewModel.swift b/Apps/BLEKitExample/Sources/Stores/BotViewModel.swift new file mode 100644 index 0000000000..1efef60854 --- /dev/null +++ b/Apps/BLEKitExample/Sources/Stores/BotViewModel.swift @@ -0,0 +1,26 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +class BotViewModel: ObservableObject { + // Bot Connect + // Make 'botIsConnected' & 'currentlyConnectedBotIndex' one prop' instead + // if currentlyConnectedBotIndex is not nil, bot is connected for sure + @Published var currentlySelectedBotIndex: Int? + @Published var currentlyConnectedBotIndex: Int? + + // Bot Advertised Information + @Published var botIsConnected: Bool = false + @Published var botChargeLevel: Double = 100 + @Published var botIsCharging: Bool = false + @Published var currentlyConnectedBotName: String = "" + @Published var botOSVersion: String = "LekaOS v1.4.0" + + func disconnect() { + self.currentlySelectedBotIndex = nil + self.currentlyConnectedBotIndex = nil + self.botIsConnected = false + } +} diff --git a/Apps/BLEKitExample/Sources/View/BLEKitExampleApp.swift b/Apps/BLEKitExample/Sources/View/BLEKitExampleApp.swift new file mode 100644 index 0000000000..225bf11b9c --- /dev/null +++ b/Apps/BLEKitExample/Sources/View/BLEKitExampleApp.swift @@ -0,0 +1,44 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import SwiftUI + +// MARK: - BLEKitExampleApp + +@main +struct BLEKitExampleApp: App { + @StateObject var bleManager: BLEManager = .live() + @StateObject var robot: Robot = .init() + @StateObject var botVM: BotViewModel = .init() + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(self.bleManager) + .environmentObject(self.robot) + .environmentObject(self.botVM) + } + } +} + +// MARK: - ContentView + +struct ContentView: View { + @EnvironmentObject var bleManager: BLEManager + @EnvironmentObject var robot: Robot + + var body: some View { + NavigationView { + if self.bleManager.connectedPeripheral != nil { + RobotView() + .navigationTitle("BLEKitExampleApp") + } else { + RobotListView() + .navigationTitle("BLEKitExampleApp") + } + } + .navigationViewStyle(StackNavigationViewStyle()) + } +} diff --git a/Apps/BLEKitExample/Sources/View/BotConnectComponents/BotFaceView.swift b/Apps/BLEKitExample/Sources/View/BotConnectComponents/BotFaceView.swift new file mode 100644 index 0000000000..b19f8a8ba5 --- /dev/null +++ b/Apps/BLEKitExample/Sources/View/BotConnectComponents/BotFaceView.swift @@ -0,0 +1,73 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// MARK: - BotFaceView + +struct BotFaceView: View { + // MARK: Internal + + @Binding var isSelected: Bool + @Binding var isConnected: Bool + @Binding var name: String + @Binding var battery: Int + @Binding var isCharging: Bool + @Binding var osVersion: String + + var body: some View { + VStack { + Image("robot_connexion_bluetooth") + .overlay(content: { + Circle() + .inset(by: -10) + .stroke( + Color(.green), + style: StrokeStyle( + lineWidth: 2, + lineCap: .butt, + lineJoin: .round, + dash: [12, 3] + ) + ) + .opacity(self.isSelected ? 1 : 0) + .rotationEffect(.degrees(self.rotation), anchor: .center) + .animation(Animation.linear(duration: 15).repeatForever(autoreverses: false), value: self.rotation) + .onAppear { + self.rotation = 360 + } + }) + .background(Color(.green), in: Circle().inset(by: self.isConnected ? -26 : 2)) + .padding(.bottom, 40) + + Text(self.name) + + Text("Battery level : \(self.battery)") + + Text("Charging Status : " + (self.isCharging ? "On" : "Off")) + + Text("OS Version : \(self.osVersion)") + } + .animation(.default, value: self.isConnected) + } + + // MARK: Private + + @State private var rotation: CGFloat = 0.0 +} + +// MARK: - BotFaceView_Previews + +struct BotFaceView_Previews: PreviewProvider { + static var previews: some View { + BotFaceView( + isSelected: .constant(true), + isConnected: .constant(true), + name: .constant("LKAL 007"), + battery: .constant(100), + isCharging: .constant(true), + osVersion: .constant("1.4.0") + ) + } +} diff --git a/Apps/BLEKitExample/Sources/View/BotConnectComponents/BotStore.swift b/Apps/BLEKitExample/Sources/View/BotConnectComponents/BotStore.swift new file mode 100644 index 0000000000..901b936762 --- /dev/null +++ b/Apps/BLEKitExample/Sources/View/BotConnectComponents/BotStore.swift @@ -0,0 +1,107 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import SwiftUI + +// MARK: - NoFeedback_ButtonStyle + +struct NoFeedback_ButtonStyle: ButtonStyle { + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + } +} + +// MARK: - BotStore + +struct BotStore: View { + // MARK: Internal + + @EnvironmentObject var bleManager: BLEManager + @EnvironmentObject var robot: Robot + @ObservedObject var botVM: BotViewModel + + var body: some View { + Group { + if self.bleManager.peripherals.count < 1 { + self.searchInvite + } else if 1...3 ~= self.bleManager.peripherals.count { + HStack(spacing: 160) { + Spacer() + self.availableBots + Spacer() + } + .onTapGesture { + self.botVM.currentlySelectedBotIndex = nil + } + } else { + let rows = Array(repeating: GridItem(), count: 2) + ScrollViewReader { proxy in + ScrollView(.horizontal) { + LazyHGrid(rows: rows, spacing: 200) { + self.availableBots + } + } + .onTapGesture { + self.botVM.currentlySelectedBotIndex = nil + } + ._safeAreaInsets(EdgeInsets(top: 0, leading: 110, bottom: 0, trailing: 80)) + .onAppear { + guard self.botVM.currentlyConnectedBotIndex != nil else { + return + } + withAnimation { proxy.scrollTo(self.botVM.currentlyConnectedBotIndex, anchor: .center) } + } + } + } + } + .frame(height: 500) + } + + // MARK: Private + + private var availableBots: some View { + ForEach(0..> 16) / 255.0 + let green = Double((hex & 0xFF00) >> 8) / 255.0 + let blue = Double((hex & 0xFF) >> 0) / 255.0 + self.init(.displayP3, red: red, green: green, blue: blue, opacity: opacity) + } +} + +// MARK: - DesignSystemApple.ColorsSwiftUIView + +extension DesignSystemApple { + struct ColorsSwiftUIView: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + Text("Primary, secondary, accent") + .font(.title2) + ColorSwiftUIView(color: .primary) + ColorSwiftUIView(color: .secondary) + ColorSwiftUIView(color: .accentColor) + + Text("Black, gray, white") + .font(.title2) + ColorSwiftUIView(color: .black) + ColorSwiftUIView(color: .gray) + ColorSwiftUIView(color: .white) + + Text("Rainbow") + .font(.title2) + ColorSwiftUIView(color: .red) + ColorSwiftUIView(color: .orange) + ColorSwiftUIView(color: .yellow) + ColorSwiftUIView(color: .green) + ColorSwiftUIView(color: .mint) + ColorSwiftUIView(color: .teal) + ColorSwiftUIView(color: .cyan) + ColorSwiftUIView(color: .blue) + ColorSwiftUIView(color: .indigo) + ColorSwiftUIView(color: .purple) + ColorSwiftUIView(color: .pink) + ColorSwiftUIView(color: .brown) + } + } + .navigationTitle("SwiftUI colors") + } + } +} + +#Preview { + DesignSystemApple.ColorsSwiftUIView() +} diff --git a/Apps/LekaActivityUIExplorer/Sources/DesignSystem/DesignSystemApple+ColorsUIKit.swift b/Apps/LekaActivityUIExplorer/Sources/DesignSystem/DesignSystemApple+ColorsUIKit.swift new file mode 100644 index 0000000000..3d09e88db3 --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/DesignSystem/DesignSystemApple+ColorsUIKit.swift @@ -0,0 +1,105 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +extension UIColor { + // swiftlint:disable:next identifier_name + var hex: String { + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + + getRed(&r, green: &g, blue: &b, alpha: &a) + + let rgb = Int(r * 255) << 16 | Int(g * 255) << 8 | Int(b * 255) << 0 + + return String(format: "#%06x", rgb) + } +} + +extension DesignSystemApple { + struct ColorsUIKitView: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + Text("Adaptable colors") + .font(.title2) + ColorView(color: .systemRed) + ColorView(color: .systemOrange) + ColorView(color: .systemYellow) + ColorView(color: .systemGreen) + ColorView(color: .systemMint) + ColorView(color: .systemTeal) + ColorView(color: .systemCyan) + ColorView(color: .systemBlue) + ColorView(color: .systemIndigo) + ColorView(color: .systemPurple) + ColorView(color: .systemPink) + ColorView(color: .systemBrown) + + Text("Adaptable gray colors") + .font(.title2) + ColorView(color: .systemGray) + ColorView(color: .systemGray2) + ColorView(color: .systemGray3) + ColorView(color: .systemGray4) + ColorView(color: .systemGray5) + ColorView(color: .systemGray6) + + Text("Fixed colors") + .font(.title2) + ColorView(color: .black) + ColorView(color: .darkGray) + ColorView(color: .gray) + ColorView(color: .lightGray) + ColorView(color: .white) + + ColorView(color: .red) + ColorView(color: .orange) + ColorView(color: .yellow) + ColorView(color: .green) + ColorView(color: .cyan) + ColorView(color: .blue) + ColorView(color: .purple) + ColorView(color: .magenta) + ColorView(color: .brown) + } + } + .navigationTitle("UIKit Colors") + } + } + + struct ColorView: View { + // MARK: Lifecycle + + init(color: UIColor) { + self.color = color + } + + // MARK: Internal + + @Environment(\.self) var environment + + let color: UIColor + + var body: some View { + HStack { + Rectangle() + .frame(width: 50, height: 50) + .clipShape(RoundedRectangle(cornerRadius: 10)) + VStack(alignment: .leading) { + Text("\(self.color.accessibilityName): \(self.color.hex.uppercased())") + Text("The quick brown fox jumps over the lazy dog") + } + } + .foregroundColor(Color(uiColor: self.color)) + } + } +} + +#Preview { + DesignSystemApple.ColorsUIKitView() +} diff --git a/Apps/LekaActivityUIExplorer/Sources/DesignSystem/DesignSystemApple+Fonts.swift b/Apps/LekaActivityUIExplorer/Sources/DesignSystem/DesignSystemApple+Fonts.swift new file mode 100644 index 0000000000..e1ea46f8a9 --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/DesignSystem/DesignSystemApple+Fonts.swift @@ -0,0 +1,32 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +extension DesignSystemApple { + struct FontsView: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + FontView(font: .largeTitle) + FontView(font: .title) + FontView(font: .title2) + FontView(font: .title3) + FontView(font: .headline) + FontView(font: .subheadline) + FontView(font: .body) + FontView(font: .callout) + FontView(font: .caption) + FontView(font: .caption2) + FontView(font: .footnote) + } + } + .navigationTitle("Apple Fonts") + } + } +} + +#Preview { + DesignSystemApple.FontsView() +} diff --git a/Apps/LekaActivityUIExplorer/Sources/DesignSystem/DesignSystemApple.swift b/Apps/LekaActivityUIExplorer/Sources/DesignSystem/DesignSystemApple.swift new file mode 100644 index 0000000000..0a9fb47646 --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/DesignSystem/DesignSystemApple.swift @@ -0,0 +1,5 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI diff --git a/Apps/LekaActivityUIExplorer/Sources/DesignSystem/DesignSystemLeka+Buttons.swift b/Apps/LekaActivityUIExplorer/Sources/DesignSystem/DesignSystemLeka+Buttons.swift new file mode 100644 index 0000000000..8e4bf9baa9 --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/DesignSystem/DesignSystemLeka+Buttons.swift @@ -0,0 +1,49 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +extension DesignSystemLeka { + struct ButtonsView: View { + let colors: [Color] = [ + Color(hex: 0xAFCE36), + Color(hex: 0x0A579B), + Color(hex: 0xCFEBFC), + ] + + var body: some View { + ScrollView(.vertical) { + VStack(alignment: .leading, spacing: 40) { + ForEach(self.colors, id: \.self) { color in + VStack(alignment: .leading) { + Button("automatic tint \(color.description)") {} + .buttonStyle(.automatic) + .tint(color) + + Button("bordered tint \(color.description)") {} + .buttonStyle(.bordered) + .tint(color) + + Button("borderedProminent tint \(color.description)") {} + .buttonStyle(.borderedProminent) + .tint(color) + + Button("borderless tint \(color.description)") {} + .buttonStyle(.borderless) + .tint(color) + + Button("custom bordered \(color.description)") {} + .buttonStyle(.robotControlBorderedButtonStyle(foreground: color, border: color)) + } + } + } + } + .navigationTitle("Leka Buttons") + } + } +} + +#Preview { + DesignSystemApple.ButtonsView() +} diff --git a/Apps/LekaActivityUIExplorer/Sources/DesignSystem/DesignSystemLeka+Colors.swift b/Apps/LekaActivityUIExplorer/Sources/DesignSystem/DesignSystemLeka+Colors.swift new file mode 100644 index 0000000000..ba7cc255c0 --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/DesignSystem/DesignSystemLeka+Colors.swift @@ -0,0 +1,26 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +extension DesignSystemLeka { + struct ColorsSwiftUIView: View { + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + Text("Dark blue, light blue, green") + .font(.title2) + ColorSwiftUIView(color: Color(hex: 0xAFCE36)) + ColorSwiftUIView(color: Color(hex: 0xCFEBFC)) + ColorSwiftUIView(color: Color(hex: 0x0A579B)) + } + } + .navigationTitle("Leka SwiftUI colors") + } + } +} + +#Preview { + DesignSystemLeka.ColorsSwiftUIView() +} diff --git a/Apps/LekaActivityUIExplorer/Sources/Experimentation/ExperimentListView.swift b/Apps/LekaActivityUIExplorer/Sources/Experimentation/ExperimentListView.swift new file mode 100644 index 0000000000..118d541d84 --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/Experimentation/ExperimentListView.swift @@ -0,0 +1,54 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import SwiftUI + +// MARK: - Experiment + +struct Experiment: Identifiable { + var id = UUID().uuidString + var name: String = "" +} + +// MARK: - ExperimentListView + +struct ExperimentListView: View { + @State var currentActivity: Experiment? + + let kExperiments: [Experiment] = [ + Experiment(name: "Sort images"), + Experiment(name: "Sort colors"), + Experiment(name: "Sequencing"), + ] + + var body: some View { + ScrollView { + VStack(spacing: 30) { + ForEach(self.kExperiments, id: \.id) { activity in + Button(activity.name) { + self.currentActivity = activity + } + } + } + .fullScreenCover(item: self.$currentActivity) { + self.currentActivity = nil + } content: { activity in + if activity.name == "Sort images" { + DragAndDropToSortImagesActivityView() + } else if activity.name == "Sort colors" { + DragAndDropToSortColorsActivityView() + } else { + DragAndDropToSequenceActivityView() + } + } + .buttonStyle(.borderedProminent) + } + .navigationTitle("Experimentation") + } +} + +#Preview { + ExperimentListView() +} diff --git a/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/CardItem.swift b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/CardItem.swift new file mode 100644 index 0000000000..4e3f90843c --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/CardItem.swift @@ -0,0 +1,36 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import SwiftUI +import UniformTypeIdentifiers + +// MARK: - CardItem + +struct CardItem: Codable, Hashable, Transferable { + static var transferRepresentation: some TransferRepresentation { + CodableRepresentation(contentType: .cardItem) + // check other representationTypes: here is Codable representation + // => Data representation + // => File representation + } + + let id: UUID + let title: String + let color: String +} + +extension UTType { + static let cardItem = UTType(exportedAs: "io.leka.apf.app.uiexplorer.sequencing.card_item") +} + +// MARK: - MockData + +enum MockData { + static let card1 = CardItem(id: UUID(), title: "Card #1", color: "xyloAttach") + static let card2 = CardItem(id: UUID(), title: "Card #2", color: "lekaOrange") + static let card3 = CardItem(id: UUID(), title: "Card #3", color: "lekaGreen") + static let card4 = CardItem(id: UUID(), title: "Card #4", color: "lekaBlue") + static let card5 = CardItem(id: UUID(), title: "Card #5", color: "bravoHighlights") +} diff --git a/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/DestinationSlotView.swift b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/DestinationSlotView.swift new file mode 100644 index 0000000000..12753f1b98 --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/DestinationSlotView.swift @@ -0,0 +1,65 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct DestinationSlotView: View { + let title: String + @Binding var cards: [CardItem] + let isTargeted: Bool + + @State var draggingItem: CardItem? + + var body: some View { + VStack(alignment: .center) { + Text(self.title).font(.footnote.bold()) + + ZStack(alignment: .top) { + RoundedRectangle(cornerRadius: 26, style: .continuous) + .frame( + minWidth: 174, + idealWidth: 174, + maxWidth: 174, + minHeight: 244, + idealHeight: 244, + maxHeight: 244, + alignment: .center + ) + .foregroundColor(self.isTargeted ? .teal.opacity(0.35) : Color(.secondarySystemFill)) + + let columns = Array(repeating: GridItem(spacing: 10), count: 1) + LazyVGrid(columns: columns, spacing: 12, content: { + ForEach(self.cards, id: \.id) { task in + DraggableTile(item: task) + .padding(12) + .draggable(task) { + DraggableTile(item: task) + .contentShape(.dragPreview, RoundedRectangle(cornerRadius: 14, + style: .continuous)) + .onAppear { + self.draggingItem = task + } + } + .dropDestination(for: CardItem.self) { _, _ in + self.draggingItem = nil + return false + } isTargeted: { status in + if let draggingItem, status, draggingItem != task { + // Moving Color from source to destination + if let sourceIndex = cards.firstIndex(of: draggingItem), + let destinationIndex = cards.firstIndex(of: task) + { + withAnimation(.default) { + let sourceItem = self.cards.remove(at: sourceIndex) + self.cards.insert(sourceItem, at: destinationIndex) + } + } + } + } + } + }) + } + } + } +} diff --git a/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/DragAndDropToSequenceActivityView.swift b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/DragAndDropToSequenceActivityView.swift new file mode 100644 index 0000000000..843e122bf8 --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/DragAndDropToSequenceActivityView.swift @@ -0,0 +1,24 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// MARK: - DragAndDropToSequenceActivityView + +struct DragAndDropToSequenceActivityView: View { + @Environment(\.dismiss) var dismiss + + var body: some View { + NavigationStack { + DragAndDropToSequenceView() + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Dismiss") { + self.dismiss() + } + } + } + } + } +} diff --git a/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/DragAndDropToSequenceView.swift b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/DragAndDropToSequenceView.swift new file mode 100644 index 0000000000..c4c60208bd --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/DragAndDropToSequenceView.swift @@ -0,0 +1,189 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// MARK: - DragAndDropToSequenceView + +struct DragAndDropToSequenceView: View { + // MARK: Internal + + var body: some View { + VStack { + Spacer() + ProposalsDeckView(title: "Proposals", cards: self.$proposals, isTargeted: self.isProposalsTargeted) + .dropDestination(for: CardItem.self) { droppedItems, _ in + for task in droppedItems { + self.card1Items.removeAll(where: { $0.id == task.id }) + self.card2Items.removeAll(where: { $0.id == task.id }) + self.card3Items.removeAll(where: { $0.id == task.id }) + self.card4Items.removeAll(where: { $0.id == task.id }) + self.card5Items.removeAll(where: { $0.id == task.id }) + } + let index = self.proposals.firstIndex(where: { $0.title == "" }) + guard let index else { + return false + } + self.copyAt(index: index, droppedItem: droppedItems[0]) + return true + } isTargeted: { isTargeted in + self.isProposalsTargeted = isTargeted + } + Spacer() + HStack(spacing: 20) { + DestinationSlotView(title: "Step #1", cards: self.$card1Items, isTargeted: self.isSlot1Targeted) + .dropDestination(for: CardItem.self) { droppedItems, _ in + guard self.card1Items.isEmpty else { + return false + } + for task in droppedItems { + self.card2Items.removeAll(where: { $0.id == task.id }) + self.card3Items.removeAll(where: { $0.id == task.id }) + self.card4Items.removeAll(where: { $0.id == task.id }) + self.card5Items.removeAll(where: { $0.id == task.id }) + guard !self.proposals.contains(where: { $0.id == task.id }) else { + let index = self.proposals.firstIndex(where: { $0.id == task.id }) + self.proposals[index!] = CardItem(id: UUID(), title: "", color: "clear") + break + } + } + self.copyTo(destination: &self.card1Items, droppedItems: droppedItems) + return true + } isTargeted: { isTargeted in + self.isSlot1Targeted = self.card1Items.isEmpty ? isTargeted : false + } + DestinationSlotView(title: "Step #2", cards: self.$card2Items, isTargeted: self.isSlot2Targeted) + .dropDestination(for: CardItem.self) { droppedItems, _ in + guard self.card2Items.isEmpty else { + return false + } + for task in droppedItems { + self.card1Items.removeAll(where: { $0.id == task.id }) + self.card3Items.removeAll(where: { $0.id == task.id }) + self.card4Items.removeAll(where: { $0.id == task.id }) + self.card5Items.removeAll(where: { $0.id == task.id }) + guard !self.proposals.contains(where: { $0.id == task.id }) else { + let index = self.proposals.firstIndex(where: { $0.id == task.id }) + self.proposals[index!] = CardItem(id: UUID(), title: "", color: "clear") + break + } + } + self.copyTo(destination: &self.card2Items, droppedItems: droppedItems) + return true + } isTargeted: { isTargeted in + self.isSlot2Targeted = self.card2Items.isEmpty ? isTargeted : false + } + DestinationSlotView(title: "Step #3", cards: self.$card3Items, isTargeted: self.isSlot3Targeted) + .dropDestination(for: CardItem.self) { droppedItems, _ in + guard self.card3Items.isEmpty else { + return false + } + for task in droppedItems { + self.card1Items.removeAll(where: { $0.id == task.id }) + self.card2Items.removeAll(where: { $0.id == task.id }) + self.card4Items.removeAll(where: { $0.id == task.id }) + self.card5Items.removeAll(where: { $0.id == task.id }) + guard !self.proposals.contains(where: { $0.id == task.id }) else { + let index = self.proposals.firstIndex(where: { $0.id == task.id }) + self.proposals[index!] = CardItem(id: UUID(), title: "", color: "clear") + break + } + } + self.copyTo(destination: &self.card3Items, droppedItems: droppedItems) + return true + } isTargeted: { isTargeted in + self.isSlot3Targeted = self.card3Items.isEmpty ? isTargeted : false + } + DestinationSlotView(title: "Step #4", cards: self.$card4Items, isTargeted: self.isSlot4Targeted) + .dropDestination(for: CardItem.self) { droppedItems, _ in + guard self.card4Items.isEmpty else { + return false + } + for task in droppedItems { + self.card1Items.removeAll(where: { $0.id == task.id }) + self.card2Items.removeAll(where: { $0.id == task.id }) + self.card3Items.removeAll(where: { $0.id == task.id }) + self.card5Items.removeAll(where: { $0.id == task.id }) + guard !self.proposals.contains(where: { $0.id == task.id }) else { + let index = self.proposals.firstIndex(where: { $0.id == task.id }) + self.proposals[index!] = CardItem(id: UUID(), title: "", color: "clear") + break + } + } + self.copyTo(destination: &self.card4Items, droppedItems: droppedItems) + return true + } isTargeted: { isTargeted in + self.isSlot4Targeted = self.card4Items.isEmpty ? isTargeted : false + } + DestinationSlotView(title: "Step #5", cards: self.$card5Items, isTargeted: self.isSlot5Targeted) + .dropDestination(for: CardItem.self) { droppedItems, _ in + guard self.card5Items.isEmpty else { + return false + } + for task in droppedItems { + self.card1Items.removeAll(where: { $0.id == task.id }) + self.card2Items.removeAll(where: { $0.id == task.id }) + self.card3Items.removeAll(where: { $0.id == task.id }) + self.card4Items.removeAll(where: { $0.id == task.id }) + guard !self.proposals.contains(where: { $0.id == task.id }) else { + let index = self.proposals.firstIndex(where: { $0.id == task.id }) + self.proposals[index!] = CardItem(id: UUID(), title: "", color: "clear") + break + } + } + self.copyTo(destination: &self.card5Items, droppedItems: droppedItems) + return true + } isTargeted: { isTargeted in + self.isSlot5Targeted = self.card5Items.isEmpty ? isTargeted : false + } + } + .padding() + } + } + + // MARK: Private + + @State private var proposals: [CardItem] = [ + MockData.card1, + MockData.card2, + MockData.card3, + MockData.card4, + MockData.card5, + ].shuffled() + @State private var card1Items: [CardItem] = [] + @State private var card2Items: [CardItem] = [] + @State private var card3Items: [CardItem] = [] + @State private var card4Items: [CardItem] = [] + @State private var card5Items: [CardItem] = [] + + @State private var isProposalsTargeted: Bool = false + @State private var isSlot1Targeted: Bool = false + @State private var isSlot2Targeted: Bool = false + @State private var isSlot3Targeted: Bool = false + @State private var isSlot4Targeted: Bool = false + @State private var isSlot5Targeted: Bool = false + + private func copyTo(destination: inout [CardItem], droppedItems: [CardItem]) { + // add a copy of the original item & make sure there is no duplicates + let joined = destination + droppedItems + let totalTasks: Set = Set(joined) + destination = Array(totalTasks) + } + + private func copyAt(index: Int, droppedItem: CardItem) { + guard !self.proposals.contains(where: { $0.id == droppedItem.id }) else { + return + } + self.proposals[index] = droppedItem + } +} + +// MARK: - ContentView_Previews + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + DragAndDropToSequenceView() + .previewInterfaceOrientation(.landscapeRight) + } +} diff --git a/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/DraggableTile.swift b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/DraggableTile.swift new file mode 100644 index 0000000000..dc1b3ec19c --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/DraggableTile.swift @@ -0,0 +1,37 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// MARK: - DraggableTile + +struct DraggableTile: View { + // MARK: Internal + + let item: CardItem + + var body: some View { + self.makeTile(item: self.item) + } + + // MARK: Private + + @ViewBuilder + private func makeTile(item: CardItem) -> some View { + Text(item.title) + .frame(width: 150, height: 220) + .background(Color(item.color), + in: RoundedRectangle(cornerRadius: 14, + style: .continuous)) + .shadow(radius: 1, x: 1, y: 1) + } +} + +// MARK: - DraggableTile_Previews + +struct DraggableTile_Previews: PreviewProvider { + static var previews: some View { + DraggableTile(item: CardItem(id: UUID(), title: "Card #", color: "lekaGreen")) + } +} diff --git a/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/ProposalsDeckView.swift b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/ProposalsDeckView.swift new file mode 100644 index 0000000000..b5504bd353 --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSequence/ProposalsDeckView.swift @@ -0,0 +1,59 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct ProposalsDeckView: View { + let title: String + @Binding var cards: [CardItem] + let isTargeted: Bool + + @State var draggingItem: CardItem? + + var body: some View { + VStack(alignment: .center) { + Text(self.title).font(.footnote.bold()) + + ZStack(alignment: .center) { + RoundedRectangle(cornerRadius: 26, style: .continuous) + .frame(maxWidth: .infinity) + .foregroundColor(self.isTargeted ? .teal.opacity(0.35) : Color(.secondarySystemFill)) + + let columns = Array(repeating: GridItem(spacing: 10), count: 5) + LazyVGrid(columns: columns, spacing: 12, content: { + ForEach(self.cards, id: \.id) { task in + DraggableTile(item: task) + .padding(12) + .draggable(task) { + DraggableTile(item: task) + .contentShape(.dragPreview, RoundedRectangle(cornerRadius: 14, + style: .continuous)) + .onAppear { + self.draggingItem = task + } + } + .dropDestination(for: CardItem.self) { _, _ in + self.draggingItem = nil + return false + } isTargeted: { status in + if let draggingItem, status, draggingItem != task { + // Moving Color from source to destination + if let sourceIndex = cards.firstIndex(of: draggingItem), + let destinationIndex = cards.firstIndex(of: task) + { + withAnimation(.default) { + let sourceItem = self.cards.remove(at: sourceIndex) + self.cards.insert(sourceItem, at: destinationIndex) + } + } + } + } + } + }) + } + } + .frame(maxWidth: .infinity, maxHeight: 244) + .padding() + } +} diff --git a/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSort/DragAndDropToSort.swift b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSort/DragAndDropToSort.swift new file mode 100644 index 0000000000..517c1d8c19 --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSort/DragAndDropToSort.swift @@ -0,0 +1,90 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// MARK: - DragAndDropToSortView + +struct DragAndDropToSortView: View { + @State var items: [MovableItemData] + @Binding var numberOfColumns: Int + @State var draggingItem: MovableItemData? + + var body: some View { + let columns = Array(repeating: GridItem(spacing: 10), count: numberOfColumns) + LazyVGrid(columns: columns, spacing: 40, content: { + ForEach(self.items, id: \.id) { item in + MovableItem(data: item) + .onDrag { + self.draggingItem = item + return NSItemProvider() + } + .onDrop(of: [.text], delegate: DropViewDelegate( + destinationItem: item, + itemCollection: self.$items, + draggedItem: self.$draggingItem, + isDragged: .constant(self.draggingItem == item) + )) + } + }) + .padding(15) + .frame(maxWidth: self.numberOfColumns == 2 ? 600 : .infinity) + .animation(.easeInOut(duration: 0.5), value: self.numberOfColumns) + } +} + +// MARK: - DropViewDelegate + +struct DropViewDelegate: DropDelegate { + // MARK: Internal + + let destinationItem: MovableItemData + @Binding var itemCollection: [MovableItemData] + @Binding var draggedItem: MovableItemData? + @Binding var isDragged: Bool + + func dropEntered(info _: DropInfo) { + self.swapItems() + } + + func dropUpdated(info _: DropInfo) -> DropProposal? { + DropProposal(operation: .move) + } + + func performDrop(info _: DropInfo) -> Bool { + self.draggedItem = nil + return true + } + + // MARK: Private + + private func swapItems() { + if let draggedItem { + let fromIndex = self.itemCollection.firstIndex(of: draggedItem) + let animation = Animation.interactiveSpring(response: 0.4, dampingFraction: 0.75, blendDuration: 0.25) + if let fromIndex { + guard let toIndex = itemCollection.firstIndex(of: destinationItem) else { + return + } + if fromIndex != toIndex { + withAnimation(animation) { + self.itemCollection + .move( + fromOffsets: IndexSet(integer: fromIndex), + toOffset: toIndex > fromIndex ? (toIndex + 1) : toIndex + ) + } + } else { + withAnimation(animation) { + self.itemCollection + .move( + fromOffsets: IndexSet(integer: fromIndex), + toOffset: toIndex + ) + } + } + } + } + } +} diff --git a/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSort/DragAndDropToSortColorsActivityView.swift b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSort/DragAndDropToSortColorsActivityView.swift new file mode 100644 index 0000000000..ba9137fa0a --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSort/DragAndDropToSortColorsActivityView.swift @@ -0,0 +1,54 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +let rainbowColors: [MovableItemData] = [ + MovableItemData(color: .purple), + MovableItemData(color: .blue), + MovableItemData(color: .mint), + MovableItemData(color: .green), + MovableItemData(color: .yellow), + MovableItemData(color: .orange), + MovableItemData(color: .red), + MovableItemData(color: .pink), +].shuffled() + +// MARK: - DragAndDropToSortColorsActivityView + +struct DragAndDropToSortColorsActivityView: View { + // MARK: Internal + + @Environment(\.dismiss) var dismiss + + var body: some View { + NavigationStack { + ZStack { + RoundedRectangle(cornerRadius: 18, style: .continuous) + .stroke(Color.secondary, lineWidth: 2) + .padding(10) + DragAndDropToSortView(items: rainbowColors, numberOfColumns: .constant(self.isGridOn ? 4 : 8)) + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Toggle(isOn: self.$isGridOn) { + Image(systemName: "rectangle.grid.3x2") + .symbolVariant(self.isGridOn ? .fill : .none) + } + .tint(.accentColor) + .padding(20) + } + ToolbarItem(placement: .topBarLeading) { + Button("Dismiss") { + self.dismiss() + } + } + } + } + } + + // MARK: Private + + @State private var isGridOn: Bool = false +} diff --git a/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSort/DragAndDropToSortImagesActivityView.swift b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSort/DragAndDropToSortImagesActivityView.swift new file mode 100644 index 0000000000..629208bb8e --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSort/DragAndDropToSortImagesActivityView.swift @@ -0,0 +1,50 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +let fruits: [MovableItemData] = [ + MovableItemData(color: .clear, image: Image("banana")), + MovableItemData(color: .clear, image: Image("avocado")), + MovableItemData(color: .clear, image: Image("cherry")), + MovableItemData(color: .clear, image: Image("watermelon")), +].shuffled() + +// MARK: - DragAndDropToSortImagesActivityView + +struct DragAndDropToSortImagesActivityView: View { + // MARK: Internal + + @Environment(\.dismiss) var dismiss + + var body: some View { + NavigationStack { + ZStack { + RoundedRectangle(cornerRadius: 18, style: .continuous) + .stroke(Color.secondary, lineWidth: 1) + .padding(10) + DragAndDropToSortView(items: fruits, numberOfColumns: .constant(self.isGridOn ? 2 : 4)) + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Toggle(isOn: self.$isGridOn) { + Image(systemName: "rectangle.grid.3x2") + .symbolVariant(self.isGridOn ? .fill : .none) + } + .tint(.accentColor) + .padding(20) + } + ToolbarItem(placement: .topBarLeading) { + Button("Dismiss") { + self.dismiss() + } + } + } + } + } + + // MARK: Private + + @State private var isGridOn: Bool = false +} diff --git a/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSort/MovableItemModel.swift b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSort/MovableItemModel.swift new file mode 100644 index 0000000000..8ea8763c41 --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/Experimentation/SwiftUISequencing/DragAndDropToSort/MovableItemModel.swift @@ -0,0 +1,35 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import SwiftUI + +// MARK: - MovableItemData + +struct MovableItemData: Identifiable, Equatable { + let id = UUID() + var color: Color + var image: Image? + var size = CGSize(width: 110, height: 110) +} + +// MARK: - MovableItem + +struct MovableItem: View { + let data: MovableItemData + + var body: some View { + Group { + if let image = data.image { + image + .resizable() + .scaledToFit() + } else { + RoundedRectangle(cornerRadius: 30, style: .continuous) + .fill(self.data.color.gradient) + } + } + .frame(width: self.data.size.width, height: self.data.size.height) + } +} diff --git a/Apps/LekaActivityUIExplorer/Sources/GEKNewSystem/GEKNewSystemView.swift b/Apps/LekaActivityUIExplorer/Sources/GEKNewSystem/GEKNewSystemView.swift new file mode 100644 index 0000000000..75a8f74cc8 --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/GEKNewSystem/GEKNewSystemView.swift @@ -0,0 +1,75 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import GameEngineKit +import SwiftUI + +let kActivities: [ActivityDeprecated] = [ + // ? Filename format + // ? touchToSelect: activity-touchToSelect-- + // ? dragAndDropIntoZones: activity-dragAndDropIntoZones--- + + ContentKit.decodeActivityDeprecated("activity-medley"), + ContentKit.decodeActivityDeprecated("activity-colorBingo"), + ContentKit.decodeActivityDeprecated("activity-danceFreeze"), + + ContentKit.decodeActivityDeprecated("activity-touchToSelect-one_right_answer-colors"), + ContentKit.decodeActivityDeprecated("activity-touchToSelect-one_right_answer-colors-shuffle_choices"), + ContentKit.decodeActivityDeprecated("activity-touchToSelect-one_right_answer-colors-shuffle_exercises"), + ContentKit.decodeActivityDeprecated("activity-touchToSelect-one_right_answer-colors-shuffle_sequences"), + ContentKit.decodeActivityDeprecated("activity-touchToSelect-one_right_answer-image"), + ContentKit.decodeActivityDeprecated("activity-touchToSelect-one_right_answer-mixed"), + ContentKit.decodeActivityDeprecated("activity-touchToSelect-multipe_right_answers-colors"), + ContentKit.decodeActivityDeprecated("activity-touchToSelect-one_right_answer-sfsymbols"), + ContentKit.decodeActivityDeprecated("activity-touchToSelect-one_right_answer-emojis"), + + ContentKit.decodeActivityDeprecated("activity-listenThenTouchToSelect-mixed-images"), + ContentKit.decodeActivityDeprecated("activity-observeThenTouchToSelect-mixed-colors"), + + ContentKit.decodeActivityDeprecated("activity-dragAndDropIntoZones-one_zone-one_right_answer-image"), + // ContentKit.decodeActivity("activity-dragAndDropIntoZones-one_zone-one_right_answer-colors"), + ContentKit.decodeActivityDeprecated("activity-dragAndDropIntoZones-two_zones-multiple_right_answers-images"), + // ContentKit.decodeActivity("activity-dragAndDropIntoZones-mixed-mixed-mixed"), + + ContentKit.decodeActivityDeprecated("activity-dragAndDropToAssociate-mixed-images"), + + ContentKit.decodeActivityDeprecated("remote-standard"), + ContentKit.decodeActivityDeprecated("remote-arrow"), + ContentKit.decodeActivityDeprecated("activity-hideAndSeek"), + ContentKit.decodeActivityDeprecated("activity-xylophone-pentatonic"), + ContentKit.decodeActivityDeprecated("activity-xylophone-heptatonic"), + ContentKit.decodeActivityDeprecated("activity-melody"), + ContentKit.decodeActivityDeprecated("activity-pairing"), +] + +// MARK: - GEKNewSystemView + +struct GEKNewSystemView: View { + @State var currentActivity: ActivityDeprecated? + + var body: some View { + ScrollView { + VStack(spacing: 30) { + ForEach(kActivities, id: \.id) { activity in + Button(activity.name) { + self.currentActivity = activity + } + } + } + .fullScreenCover(item: self.$currentActivity) { + self.currentActivity = nil + } content: { activity in + ActivityView(viewModel: ActivityViewViewModel(activity: activity)) + } + .buttonStyle(.borderedProminent) + .frame(maxWidth: .infinity) + } + .navigationTitle("List of Activities") + } +} + +#Preview { + GEKNewSystemView() +} diff --git a/Apps/LekaActivityUIExplorer/Sources/MainApp.swift b/Apps/LekaActivityUIExplorer/Sources/MainApp.swift new file mode 100644 index 0000000000..5547259522 --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/MainApp.swift @@ -0,0 +1,24 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +@main +struct LekaActivityUIExplorerApp: App { + @Environment(\.colorScheme) var colorScheme + @StateObject var styleManager: StyleManager = .init() + + var body: some Scene { + WindowGroup { + NavigationView() + .tint(self.styleManager.accentColor) + .preferredColorScheme(self.styleManager.colorScheme) + .environmentObject(self.styleManager) + .onAppear { + self.styleManager.setDefaultColorScheme(self.colorScheme) + } + } + } +} diff --git a/Apps/LekaActivityUIExplorer/Sources/Navigation/Navigation.swift b/Apps/LekaActivityUIExplorer/Sources/Navigation/Navigation.swift new file mode 100644 index 0000000000..c2759c55f7 --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/Navigation/Navigation.swift @@ -0,0 +1,71 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// MARK: - Category + +enum Category: Hashable, Identifiable, CaseIterable { + case home + + case activities + case experimentations + + case designSystemAppleFonts + case designSystemAppleButtons + case designSystemAppleColorsSwiftUI + case designSystemAppleColorsUIKit + + case designSystemLekaButtons + case designSystemLekaColorsSwiftUI + + // MARK: Internal + + var id: Self { self } +} + +// MARK: - Navigation + +class Navigation: ObservableObject { + // MARK: Public + + public var selectedCategory: Category? = .home { + willSet { + self.disableUICompletly = true + // ? Note: early return to avoid reseting path + guard !self.isProgrammaticNavigation else { return } + // backupPath(for: selectedCategory) + } + didSet { + // restorePath(for: selectedCategory) + } + } + + // MARK: Internal + + static let shared = Navigation() + + @Published var disableUICompletly: Bool = false + + @Published var categories = Category.allCases + + @Published var path: NavigationPath = .init() { + willSet { + self.disableUICompletly = true + } + didSet { + self.disableUICompletly = false + } + } + + // MARK: Private + + private var isProgrammaticNavigation: Bool = false + + private var pushPopNoAnimationTransaction: Transaction { + var transaction = Transaction(animation: nil) + transaction.disablesAnimations = true + return transaction + } +} diff --git a/Apps/LekaActivityUIExplorer/Sources/Navigation/NavigationView.swift b/Apps/LekaActivityUIExplorer/Sources/Navigation/NavigationView.swift new file mode 100644 index 0000000000..e8c4a8cccf --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Sources/Navigation/NavigationView.swift @@ -0,0 +1,204 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import DesignKit +import RobotKit +import SwiftUI + +// MARK: - NavigationViewViewModel + +class NavigationViewViewModel: ObservableObject { + // MARK: Lifecycle + + init() { + Robot.shared.isConnected + .receive(on: DispatchQueue.main) + .sink { [weak self] isConnected in + guard let self else { return } + self.isRobotConnect = isConnected + } + .store(in: &self.cancellables) + } + + // MARK: Internal + + @Published var isDesignSystemAppleExpanded: Bool = false + @Published var isDesignSystemLekaExpanded: Bool = false + + @Published var isRobotConnectionPresented: Bool = false + + @Published var isRobotConnect: Bool = false + + // MARK: Private + + private var cancellables: Set = [] +} + +// MARK: - NavigationView + +struct NavigationView: View { + @EnvironmentObject var styleManager: StyleManager + + @ObservedObject var navigation: Navigation = .shared + @StateObject var viewModel: NavigationViewViewModel = .init() + + var body: some View { + NavigationSplitView { + List(selection: self.$navigation.selectedCategory) { + CategoryLabel(category: .home) + + Button { + self.viewModel.isRobotConnectionPresented.toggle() + } label: { + Label(self.viewModel.isRobotConnect ? "Disconnect robot" : "Connect robot", systemImage: "link") + .foregroundStyle(self.viewModel.isRobotConnect ? .green : .orange) + } + + Section("Activities") { + CategoryLabel(category: .activities) + CategoryLabel(category: .experimentations) + } + + Section("Design System (Apple)", isExpanded: self.$viewModel.isDesignSystemAppleExpanded) { + CategoryLabel(category: .designSystemAppleFonts) + CategoryLabel(category: .designSystemAppleButtons) + CategoryLabel(category: .designSystemAppleColorsSwiftUI) + CategoryLabel(category: .designSystemAppleColorsUIKit) + } + + Section("Design System (Leka)", isExpanded: self.$viewModel.isDesignSystemLekaExpanded) { + CategoryLabel(category: .designSystemLekaButtons) + CategoryLabel(category: .designSystemLekaColorsSwiftUI) + } + } + // TODO: (@ladislas) remove if not necessary + // .disabled(navigation.disableUICompletly) + .navigationTitle("Categories") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + self.styleManager.toggleAccentColor() + } label: { + Image(systemName: "eyedropper") + } + } + + ToolbarItem(placement: .topBarTrailing) { + Button { + self.styleManager.toggleColorScheme() + } label: { + Image(systemName: "circle.lefthalf.filled") + } + } + } + } detail: { + NavigationStack(path: self.$navigation.path) { + switch self.navigation.selectedCategory { + case .home: + Text("Hello, Home!") + .font(.largeTitle) + .bold() + + case .activities: + GEKNewSystemView() + + case .experimentations: + ExperimentListView() + + case .designSystemAppleFonts: + DesignSystemApple.FontsView() + + case .designSystemAppleButtons: + DesignSystemApple.ButtonsView() + + case .designSystemAppleColorsSwiftUI: + DesignSystemApple.ColorsSwiftUIView() + + case .designSystemAppleColorsUIKit: + DesignSystemApple.ColorsUIKitView() + + case .designSystemLekaButtons: + DesignSystemLeka.ButtonsView() + + case .designSystemLekaColorsSwiftUI: + DesignSystemLeka.ColorsSwiftUIView() + + case .none: + Text("Select a category") + } + } + } + .fullScreenCover(isPresented: self.$viewModel.isRobotConnectionPresented) { + RobotConnectionView(viewModel: RobotConnectionViewModel()) + } + } +} + +// MARK: - CategoryLabel + +struct CategoryLabel: View { + // MARK: Lifecycle + + init(category: Category) { + self.category = category + + switch category { + case .home: + self.title = "Home" + self.systemImage = "house" + + case .activities: + self.title = "Activities" + self.systemImage = "dice" + + case .experimentations: + self.title = "Experimentation" + self.systemImage = "flask" + + case .designSystemAppleFonts: + self.title = "Apple Fonts" + self.systemImage = "textformat" + + case .designSystemAppleButtons: + self.title = "Apple Buttons" + self.systemImage = "button.horizontal" + + case .designSystemAppleColorsSwiftUI: + self.title = "Apple Colors SwiftUI" + self.systemImage = "swatchpalette.fill" + + case .designSystemAppleColorsUIKit: + self.title = "Apple Colors UIKit" + self.systemImage = "swatchpalette" + + case .designSystemLekaButtons: + self.title = "Leka Buttons" + self.systemImage = "button.horizontal" + + case .designSystemLekaColorsSwiftUI: + self.title = "Leka Colors SwiftUI" + self.systemImage = "swatchpalette.fill" + } + } + + // MARK: Internal + + let category: Category + let title: String + let systemImage: String + + var body: some View { + Label(self.title, systemImage: self.systemImage) + .tag(self.category) + } +} + +// MARK: - FormView_Previews + +#Preview { + NavigationView() + .previewInterfaceOrientation(.landscapeLeft) + .environmentObject(StyleManager()) +} diff --git a/Apps/LekaActivityUIExplorer/Tests/LekaActivityGenerator_Tests.swift b/Apps/LekaActivityUIExplorer/Tests/LekaActivityGenerator_Tests.swift new file mode 100644 index 0000000000..bb158bb3b8 --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Tests/LekaActivityGenerator_Tests.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +final class LekaActivityUIExplorer_Tests: XCTestCase { + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/Apps/LekaApp/Project.swift b/Apps/LekaApp/Project.swift index aab49c617b..fef6118cfe 100644 --- a/Apps/LekaApp/Project.swift +++ b/Apps/LekaApp/Project.swift @@ -1,13 +1,34 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap +// Leka - iOS Monorepo +// Copyright APF France handicap // SPDX-License-Identifier: Apache-2.0 +// swiftformat:disable acronyms + import ProjectDescription import ProjectDescriptionHelpers -// Creates our project using a helper function defined in ProjectDescriptionHelpers -let project = Project.app(name: "LekaApp", - platform: .iOS, - dependencies: [ - .project(target: "CoreUI", path: Path("../../Modules/CoreUI")), - ]) +let project = Project.app( + name: "LekaApp", + version: "1.0.0", + infoPlist: [ + "LSApplicationCategoryType": "public.app-category.education", + "CFBundleURLTypes": [ + [ + "CFBundleTypeRole": "Editor", + "CFBundleURLName": "io.leka.apf.app.LekaApp", + "CFBundleURLSchemes": ["LekaApp"], + ], + ], + "LSApplicationQueriesSchemes": ["LekaUpdater"], + ], + dependencies: [ + .project(target: "DesignKit", path: Path("../../Modules/DesignKit")), + .project(target: "RobotKit", path: Path("../../Modules/RobotKit")), + .project(target: "AccountKit", path: Path("../../Modules/AccountKit")), + .project(target: "ContentKit", path: Path("../../Modules/ContentKit")), + .project(target: "GameEngineKit", path: Path("../../Modules/GameEngineKit")), + .external(name: "Yams"), + .external(name: "MarkdownUI"), + .external(name: "Fit"), + ] +) diff --git a/Apps/LekaApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897008..1f23a6afa4 100644 --- a/Apps/LekaApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Apps/LekaApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,15 @@ { "colors" : [ { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0x9B", + "green" : "0x57", + "red" : "0x0A" + } + }, "idiom" : "universal" } ], diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-hugo-670fed67-03ff-405b-a160-87b50065f7fc.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-hugo-670fed67-03ff-405b-a160-87b50065f7fc.imageset/Contents.json new file mode 100644 index 0000000000..ea809cf5cc --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-hugo-670fed67-03ff-405b-a160-87b50065f7fc.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-1_images_1_types-hugo-670fed67-03ff-405b-a160-87b50065f7fc.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-hugo-670fed67-03ff-405b-a160-87b50065f7fc.imageset/emotion_recognition-drawings-1_images_1_types-hugo-670fed67-03ff-405b-a160-87b50065f7fc.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-hugo-670fed67-03ff-405b-a160-87b50065f7fc.imageset/emotion_recognition-drawings-1_images_1_types-hugo-670fed67-03ff-405b-a160-87b50065f7fc.pdf new file mode 100644 index 0000000000..ff233e1f8c Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-hugo-670fed67-03ff-405b-a160-87b50065f7fc.imageset/emotion_recognition-drawings-1_images_1_types-hugo-670fed67-03ff-405b-a160-87b50065f7fc.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-louna-4a1ea3ed-522c-4c2e-a414-1d52f01daeee.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-louna-4a1ea3ed-522c-4c2e-a414-1d52f01daeee.imageset/Contents.json new file mode 100644 index 0000000000..a828748999 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-louna-4a1ea3ed-522c-4c2e-a414-1d52f01daeee.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-1_images_1_types-louna-4a1ea3ed-522c-4c2e-a414-1d52f01daeee.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-louna-4a1ea3ed-522c-4c2e-a414-1d52f01daeee.imageset/emotion_recognition-drawings-1_images_1_types-louna-4a1ea3ed-522c-4c2e-a414-1d52f01daeee.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-louna-4a1ea3ed-522c-4c2e-a414-1d52f01daeee.imageset/emotion_recognition-drawings-1_images_1_types-louna-4a1ea3ed-522c-4c2e-a414-1d52f01daeee.pdf new file mode 100644 index 0000000000..12c29c04cb Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-louna-4a1ea3ed-522c-4c2e-a414-1d52f01daeee.imageset/emotion_recognition-drawings-1_images_1_types-louna-4a1ea3ed-522c-4c2e-a414-1d52f01daeee.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-moussa-6fc67415-3dcf-4e99-b987-2e8e80ee4e6a.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-moussa-6fc67415-3dcf-4e99-b987-2e8e80ee4e6a.imageset/Contents.json new file mode 100644 index 0000000000..887aa009bd --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-moussa-6fc67415-3dcf-4e99-b987-2e8e80ee4e6a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-1_images_1_types-moussa-6fc67415-3dcf-4e99-b987-2e8e80ee4e6a.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-moussa-6fc67415-3dcf-4e99-b987-2e8e80ee4e6a.imageset/emotion_recognition-drawings-1_images_1_types-moussa-6fc67415-3dcf-4e99-b987-2e8e80ee4e6a.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-moussa-6fc67415-3dcf-4e99-b987-2e8e80ee4e6a.imageset/emotion_recognition-drawings-1_images_1_types-moussa-6fc67415-3dcf-4e99-b987-2e8e80ee4e6a.pdf new file mode 100644 index 0000000000..b02699ffea Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-moussa-6fc67415-3dcf-4e99-b987-2e8e80ee4e6a.imageset/emotion_recognition-drawings-1_images_1_types-moussa-6fc67415-3dcf-4e99-b987-2e8e80ee4e6a.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-oscar-8f0db977-c06e-4ccd-8fcb-f75baea54b8e.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-oscar-8f0db977-c06e-4ccd-8fcb-f75baea54b8e.imageset/Contents.json new file mode 100644 index 0000000000..76b79eed9f --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-oscar-8f0db977-c06e-4ccd-8fcb-f75baea54b8e.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-1_images_1_types-oscar-8f0db977-c06e-4ccd-8fcb-f75baea54b8e.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-oscar-8f0db977-c06e-4ccd-8fcb-f75baea54b8e.imageset/emotion_recognition-drawings-1_images_1_types-oscar-8f0db977-c06e-4ccd-8fcb-f75baea54b8e.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-oscar-8f0db977-c06e-4ccd-8fcb-f75baea54b8e.imageset/emotion_recognition-drawings-1_images_1_types-oscar-8f0db977-c06e-4ccd-8fcb-f75baea54b8e.pdf new file mode 100644 index 0000000000..43f9eefcb2 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-oscar-8f0db977-c06e-4ccd-8fcb-f75baea54b8e.imageset/emotion_recognition-drawings-1_images_1_types-oscar-8f0db977-c06e-4ccd-8fcb-f75baea54b8e.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-zoe-3d3c6b18-1332-4ce2-959a-3dd3343c13c1.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-zoe-3d3c6b18-1332-4ce2-959a-3dd3343c13c1.imageset/Contents.json new file mode 100644 index 0000000000..7bdb80f634 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-zoe-3d3c6b18-1332-4ce2-959a-3dd3343c13c1.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-1_images_1_types-zoe-3d3c6b18-1332-4ce2-959a-3dd3343c13c1.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-zoe-3d3c6b18-1332-4ce2-959a-3dd3343c13c1.imageset/emotion_recognition-drawings-1_images_1_types-zoe-3d3c6b18-1332-4ce2-959a-3dd3343c13c1.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-zoe-3d3c6b18-1332-4ce2-959a-3dd3343c13c1.imageset/emotion_recognition-drawings-1_images_1_types-zoe-3d3c6b18-1332-4ce2-959a-3dd3343c13c1.pdf new file mode 100644 index 0000000000..09844036f9 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-1_images_1_types-zoe-3d3c6b18-1332-4ce2-959a-3dd3343c13c1.imageset/emotion_recognition-drawings-1_images_1_types-zoe-3d3c6b18-1332-4ce2-959a-3dd3343c13c1.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-hugo-3e2d5e0e-ba42-4a4b-8ce3-ce3855bfa207.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-hugo-3e2d5e0e-ba42-4a4b-8ce3-ce3855bfa207.imageset/Contents.json new file mode 100644 index 0000000000..36eff18274 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-hugo-3e2d5e0e-ba42-4a4b-8ce3-ce3855bfa207.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-2_images_1_types-hugo-3e2d5e0e-ba42-4a4b-8ce3-ce3855bfa207.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-hugo-3e2d5e0e-ba42-4a4b-8ce3-ce3855bfa207.imageset/emotion_recognition-drawings-2_images_1_types-hugo-3e2d5e0e-ba42-4a4b-8ce3-ce3855bfa207.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-hugo-3e2d5e0e-ba42-4a4b-8ce3-ce3855bfa207.imageset/emotion_recognition-drawings-2_images_1_types-hugo-3e2d5e0e-ba42-4a4b-8ce3-ce3855bfa207.pdf new file mode 100644 index 0000000000..f7cd461bc6 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-hugo-3e2d5e0e-ba42-4a4b-8ce3-ce3855bfa207.imageset/emotion_recognition-drawings-2_images_1_types-hugo-3e2d5e0e-ba42-4a4b-8ce3-ce3855bfa207.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-louna-9d9e50c5-c3ee-442f-a8f6-227a9430fc27.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-louna-9d9e50c5-c3ee-442f-a8f6-227a9430fc27.imageset/Contents.json new file mode 100644 index 0000000000..886f387903 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-louna-9d9e50c5-c3ee-442f-a8f6-227a9430fc27.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-2_images_1_types-louna-9d9e50c5-c3ee-442f-a8f6-227a9430fc27.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-louna-9d9e50c5-c3ee-442f-a8f6-227a9430fc27.imageset/emotion_recognition-drawings-2_images_1_types-louna-9d9e50c5-c3ee-442f-a8f6-227a9430fc27.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-louna-9d9e50c5-c3ee-442f-a8f6-227a9430fc27.imageset/emotion_recognition-drawings-2_images_1_types-louna-9d9e50c5-c3ee-442f-a8f6-227a9430fc27.pdf new file mode 100644 index 0000000000..3f1b087491 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-louna-9d9e50c5-c3ee-442f-a8f6-227a9430fc27.imageset/emotion_recognition-drawings-2_images_1_types-louna-9d9e50c5-c3ee-442f-a8f6-227a9430fc27.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-moussa-6b25ba9b-7602-473a-ab17-a3c0ccc55342.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-moussa-6b25ba9b-7602-473a-ab17-a3c0ccc55342.imageset/Contents.json new file mode 100644 index 0000000000..ec0a2d8c83 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-moussa-6b25ba9b-7602-473a-ab17-a3c0ccc55342.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-2_images_1_types-moussa-6b25ba9b-7602-473a-ab17-a3c0ccc55342.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-moussa-6b25ba9b-7602-473a-ab17-a3c0ccc55342.imageset/emotion_recognition-drawings-2_images_1_types-moussa-6b25ba9b-7602-473a-ab17-a3c0ccc55342.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-moussa-6b25ba9b-7602-473a-ab17-a3c0ccc55342.imageset/emotion_recognition-drawings-2_images_1_types-moussa-6b25ba9b-7602-473a-ab17-a3c0ccc55342.pdf new file mode 100644 index 0000000000..b4ae567560 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-moussa-6b25ba9b-7602-473a-ab17-a3c0ccc55342.imageset/emotion_recognition-drawings-2_images_1_types-moussa-6b25ba9b-7602-473a-ab17-a3c0ccc55342.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-oscar-192bf178-9c89-4ef0-bbe3-84e471a0ee4f.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-oscar-192bf178-9c89-4ef0-bbe3-84e471a0ee4f.imageset/Contents.json new file mode 100644 index 0000000000..f0dc67a153 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-oscar-192bf178-9c89-4ef0-bbe3-84e471a0ee4f.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-2_images_1_types-oscar-192bf178-9c89-4ef0-bbe3-84e471a0ee4f.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-oscar-192bf178-9c89-4ef0-bbe3-84e471a0ee4f.imageset/emotion_recognition-drawings-2_images_1_types-oscar-192bf178-9c89-4ef0-bbe3-84e471a0ee4f.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-oscar-192bf178-9c89-4ef0-bbe3-84e471a0ee4f.imageset/emotion_recognition-drawings-2_images_1_types-oscar-192bf178-9c89-4ef0-bbe3-84e471a0ee4f.pdf new file mode 100644 index 0000000000..9cd286eca5 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-oscar-192bf178-9c89-4ef0-bbe3-84e471a0ee4f.imageset/emotion_recognition-drawings-2_images_1_types-oscar-192bf178-9c89-4ef0-bbe3-84e471a0ee4f.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-zoe-7b77ed81-845c-4a86-adee-66de91f51352.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-zoe-7b77ed81-845c-4a86-adee-66de91f51352.imageset/Contents.json new file mode 100644 index 0000000000..22d343a767 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-zoe-7b77ed81-845c-4a86-adee-66de91f51352.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-2_images_1_types-zoe-7b77ed81-845c-4a86-adee-66de91f51352.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-zoe-7b77ed81-845c-4a86-adee-66de91f51352.imageset/emotion_recognition-drawings-2_images_1_types-zoe-7b77ed81-845c-4a86-adee-66de91f51352.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-zoe-7b77ed81-845c-4a86-adee-66de91f51352.imageset/emotion_recognition-drawings-2_images_1_types-zoe-7b77ed81-845c-4a86-adee-66de91f51352.pdf new file mode 100644 index 0000000000..83f0e0114e Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_1_types-zoe-7b77ed81-845c-4a86-adee-66de91f51352.imageset/emotion_recognition-drawings-2_images_1_types-zoe-7b77ed81-845c-4a86-adee-66de91f51352.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-moussa_hugo-be811548-16ac-4564-886c-1cca7a6f7a67.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-moussa_hugo-be811548-16ac-4564-886c-1cca7a6f7a67.imageset/Contents.json new file mode 100644 index 0000000000..0e0dad8b7b --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-moussa_hugo-be811548-16ac-4564-886c-1cca7a6f7a67.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-2_images_2_types-moussa_hugo-be811548-16ac-4564-886c-1cca7a6f7a67.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-moussa_hugo-be811548-16ac-4564-886c-1cca7a6f7a67.imageset/emotion_recognition-drawings-2_images_2_types-moussa_hugo-be811548-16ac-4564-886c-1cca7a6f7a67.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-moussa_hugo-be811548-16ac-4564-886c-1cca7a6f7a67.imageset/emotion_recognition-drawings-2_images_2_types-moussa_hugo-be811548-16ac-4564-886c-1cca7a6f7a67.pdf new file mode 100644 index 0000000000..499af7fe0c Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-moussa_hugo-be811548-16ac-4564-886c-1cca7a6f7a67.imageset/emotion_recognition-drawings-2_images_2_types-moussa_hugo-be811548-16ac-4564-886c-1cca7a6f7a67.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-oscar_hugo-b8ccd013-c2f0-4975-82c2-50bb4cc59d26.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-oscar_hugo-b8ccd013-c2f0-4975-82c2-50bb4cc59d26.imageset/Contents.json new file mode 100644 index 0000000000..73a6272fd7 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-oscar_hugo-b8ccd013-c2f0-4975-82c2-50bb4cc59d26.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-2_images_2_types-oscar_hugo-b8ccd013-c2f0-4975-82c2-50bb4cc59d26.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-oscar_hugo-b8ccd013-c2f0-4975-82c2-50bb4cc59d26.imageset/emotion_recognition-drawings-2_images_2_types-oscar_hugo-b8ccd013-c2f0-4975-82c2-50bb4cc59d26.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-oscar_hugo-b8ccd013-c2f0-4975-82c2-50bb4cc59d26.imageset/emotion_recognition-drawings-2_images_2_types-oscar_hugo-b8ccd013-c2f0-4975-82c2-50bb4cc59d26.pdf new file mode 100644 index 0000000000..f6bc4ccb69 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-oscar_hugo-b8ccd013-c2f0-4975-82c2-50bb4cc59d26.imageset/emotion_recognition-drawings-2_images_2_types-oscar_hugo-b8ccd013-c2f0-4975-82c2-50bb4cc59d26.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-oscar_louna-a5b964de-26d8-47b2-9ae0-333e4ba97e83.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-oscar_louna-a5b964de-26d8-47b2-9ae0-333e4ba97e83.imageset/Contents.json new file mode 100644 index 0000000000..7e2c4db2b3 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-oscar_louna-a5b964de-26d8-47b2-9ae0-333e4ba97e83.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-2_images_2_types-oscar_louna-a5b964de-26d8-47b2-9ae0-333e4ba97e83.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-oscar_louna-a5b964de-26d8-47b2-9ae0-333e4ba97e83.imageset/emotion_recognition-drawings-2_images_2_types-oscar_louna-a5b964de-26d8-47b2-9ae0-333e4ba97e83.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-oscar_louna-a5b964de-26d8-47b2-9ae0-333e4ba97e83.imageset/emotion_recognition-drawings-2_images_2_types-oscar_louna-a5b964de-26d8-47b2-9ae0-333e4ba97e83.pdf new file mode 100644 index 0000000000..3a6f6e619c Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-oscar_louna-a5b964de-26d8-47b2-9ae0-333e4ba97e83.imageset/emotion_recognition-drawings-2_images_2_types-oscar_louna-a5b964de-26d8-47b2-9ae0-333e4ba97e83.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-zoe_hugo-53c26de7-7201-46c6-a558-03daac584f7d.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-zoe_hugo-53c26de7-7201-46c6-a558-03daac584f7d.imageset/Contents.json new file mode 100644 index 0000000000..df964975f4 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-zoe_hugo-53c26de7-7201-46c6-a558-03daac584f7d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-2_images_2_types-zoe_hugo-53c26de7-7201-46c6-a558-03daac584f7d.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-zoe_hugo-53c26de7-7201-46c6-a558-03daac584f7d.imageset/emotion_recognition-drawings-2_images_2_types-zoe_hugo-53c26de7-7201-46c6-a558-03daac584f7d.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-zoe_hugo-53c26de7-7201-46c6-a558-03daac584f7d.imageset/emotion_recognition-drawings-2_images_2_types-zoe_hugo-53c26de7-7201-46c6-a558-03daac584f7d.pdf new file mode 100644 index 0000000000..608991c92c Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-zoe_hugo-53c26de7-7201-46c6-a558-03daac584f7d.imageset/emotion_recognition-drawings-2_images_2_types-zoe_hugo-53c26de7-7201-46c6-a558-03daac584f7d.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-zoe_louna-726d32a3-4a3e-4085-a921-4847af624ea0.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-zoe_louna-726d32a3-4a3e-4085-a921-4847af624ea0.imageset/Contents.json new file mode 100644 index 0000000000..88f48d1b61 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-zoe_louna-726d32a3-4a3e-4085-a921-4847af624ea0.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-2_images_2_types-zoe_louna-726d32a3-4a3e-4085-a921-4847af624ea0.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-zoe_louna-726d32a3-4a3e-4085-a921-4847af624ea0.imageset/emotion_recognition-drawings-2_images_2_types-zoe_louna-726d32a3-4a3e-4085-a921-4847af624ea0.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-zoe_louna-726d32a3-4a3e-4085-a921-4847af624ea0.imageset/emotion_recognition-drawings-2_images_2_types-zoe_louna-726d32a3-4a3e-4085-a921-4847af624ea0.pdf new file mode 100644 index 0000000000..3df00e8f3c Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-2_images_2_types-zoe_louna-726d32a3-4a3e-4085-a921-4847af624ea0.imageset/emotion_recognition-drawings-2_images_2_types-zoe_louna-726d32a3-4a3e-4085-a921-4847af624ea0.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-hugo-2ddfa5c9-b3b2-43b3-97f2-70fa576b4fc7.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-hugo-2ddfa5c9-b3b2-43b3-97f2-70fa576b4fc7.imageset/Contents.json new file mode 100644 index 0000000000..7006dedfa2 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-hugo-2ddfa5c9-b3b2-43b3-97f2-70fa576b4fc7.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-4_images_1_types-hugo-2ddfa5c9-b3b2-43b3-97f2-70fa576b4fc7.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-hugo-2ddfa5c9-b3b2-43b3-97f2-70fa576b4fc7.imageset/emotion_recognition-drawings-4_images_1_types-hugo-2ddfa5c9-b3b2-43b3-97f2-70fa576b4fc7.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-hugo-2ddfa5c9-b3b2-43b3-97f2-70fa576b4fc7.imageset/emotion_recognition-drawings-4_images_1_types-hugo-2ddfa5c9-b3b2-43b3-97f2-70fa576b4fc7.pdf new file mode 100644 index 0000000000..c7ba36244c Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-hugo-2ddfa5c9-b3b2-43b3-97f2-70fa576b4fc7.imageset/emotion_recognition-drawings-4_images_1_types-hugo-2ddfa5c9-b3b2-43b3-97f2-70fa576b4fc7.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-louna-f247c96a-e980-42ca-ae26-71fc6b1667e3.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-louna-f247c96a-e980-42ca-ae26-71fc6b1667e3.imageset/Contents.json new file mode 100644 index 0000000000..b2aef9f30c --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-louna-f247c96a-e980-42ca-ae26-71fc6b1667e3.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-4_images_1_types-louna-f247c96a-e980-42ca-ae26-71fc6b1667e3.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-louna-f247c96a-e980-42ca-ae26-71fc6b1667e3.imageset/emotion_recognition-drawings-4_images_1_types-louna-f247c96a-e980-42ca-ae26-71fc6b1667e3.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-louna-f247c96a-e980-42ca-ae26-71fc6b1667e3.imageset/emotion_recognition-drawings-4_images_1_types-louna-f247c96a-e980-42ca-ae26-71fc6b1667e3.pdf new file mode 100644 index 0000000000..3fe15db98a Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-louna-f247c96a-e980-42ca-ae26-71fc6b1667e3.imageset/emotion_recognition-drawings-4_images_1_types-louna-f247c96a-e980-42ca-ae26-71fc6b1667e3.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-moussa-76c561fa-6c1a-4cb9-8244-598bb558767a.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-moussa-76c561fa-6c1a-4cb9-8244-598bb558767a.imageset/Contents.json new file mode 100644 index 0000000000..f487d6b476 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-moussa-76c561fa-6c1a-4cb9-8244-598bb558767a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-4_images_1_types-moussa-76c561fa-6c1a-4cb9-8244-598bb558767a.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-moussa-76c561fa-6c1a-4cb9-8244-598bb558767a.imageset/emotion_recognition-drawings-4_images_1_types-moussa-76c561fa-6c1a-4cb9-8244-598bb558767a.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-moussa-76c561fa-6c1a-4cb9-8244-598bb558767a.imageset/emotion_recognition-drawings-4_images_1_types-moussa-76c561fa-6c1a-4cb9-8244-598bb558767a.pdf new file mode 100644 index 0000000000..21489be4a9 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-moussa-76c561fa-6c1a-4cb9-8244-598bb558767a.imageset/emotion_recognition-drawings-4_images_1_types-moussa-76c561fa-6c1a-4cb9-8244-598bb558767a.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-oscar-e76730cc-585e-4a1f-8f75-37b9705e4ba8.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-oscar-e76730cc-585e-4a1f-8f75-37b9705e4ba8.imageset/Contents.json new file mode 100644 index 0000000000..c03db32bd8 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-oscar-e76730cc-585e-4a1f-8f75-37b9705e4ba8.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-4_images_1_types-oscar-e76730cc-585e-4a1f-8f75-37b9705e4ba8.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-oscar-e76730cc-585e-4a1f-8f75-37b9705e4ba8.imageset/emotion_recognition-drawings-4_images_1_types-oscar-e76730cc-585e-4a1f-8f75-37b9705e4ba8.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-oscar-e76730cc-585e-4a1f-8f75-37b9705e4ba8.imageset/emotion_recognition-drawings-4_images_1_types-oscar-e76730cc-585e-4a1f-8f75-37b9705e4ba8.pdf new file mode 100644 index 0000000000..7f309f252c Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-oscar-e76730cc-585e-4a1f-8f75-37b9705e4ba8.imageset/emotion_recognition-drawings-4_images_1_types-oscar-e76730cc-585e-4a1f-8f75-37b9705e4ba8.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-zoe-f8bed230-1c22-4530-b7cf-775ab3f5342b.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-zoe-f8bed230-1c22-4530-b7cf-775ab3f5342b.imageset/Contents.json new file mode 100644 index 0000000000..d5f33c5e2d --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-zoe-f8bed230-1c22-4530-b7cf-775ab3f5342b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-4_images_1_types-zoe-f8bed230-1c22-4530-b7cf-775ab3f5342b.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-zoe-f8bed230-1c22-4530-b7cf-775ab3f5342b.imageset/emotion_recognition-drawings-4_images_1_types-zoe-f8bed230-1c22-4530-b7cf-775ab3f5342b.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-zoe-f8bed230-1c22-4530-b7cf-775ab3f5342b.imageset/emotion_recognition-drawings-4_images_1_types-zoe-f8bed230-1c22-4530-b7cf-775ab3f5342b.pdf new file mode 100644 index 0000000000..fc64a79681 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-zoe-f8bed230-1c22-4530-b7cf-775ab3f5342b.imageset/emotion_recognition-drawings-4_images_1_types-zoe-f8bed230-1c22-4530-b7cf-775ab3f5342b.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-zoe_oscar_louna_moussa_hugo-e0c4103a-8ce5-4567-b460-579ad5cdc678.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-zoe_oscar_louna_moussa_hugo-e0c4103a-8ce5-4567-b460-579ad5cdc678.imageset/Contents.json new file mode 100644 index 0000000000..c38906ae24 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-zoe_oscar_louna_moussa_hugo-e0c4103a-8ce5-4567-b460-579ad5cdc678.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-4_images_1_types-zoe_oscar_louna_moussa_hugo-e0c4103a-8ce5-4567-b460-579ad5cdc678.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-zoe_oscar_louna_moussa_hugo-e0c4103a-8ce5-4567-b460-579ad5cdc678.imageset/emotion_recognition-drawings-4_images_1_types-zoe_oscar_louna_moussa_hugo-e0c4103a-8ce5-4567-b460-579ad5cdc678.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-zoe_oscar_louna_moussa_hugo-e0c4103a-8ce5-4567-b460-579ad5cdc678.imageset/emotion_recognition-drawings-4_images_1_types-zoe_oscar_louna_moussa_hugo-e0c4103a-8ce5-4567-b460-579ad5cdc678.pdf new file mode 100644 index 0000000000..d68529bd82 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_1_types-zoe_oscar_louna_moussa_hugo-e0c4103a-8ce5-4567-b460-579ad5cdc678.imageset/emotion_recognition-drawings-4_images_1_types-zoe_oscar_louna_moussa_hugo-e0c4103a-8ce5-4567-b460-579ad5cdc678.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_hugo-98dcf69e-570a-4abf-a227-22704dfa10b8.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_hugo-98dcf69e-570a-4abf-a227-22704dfa10b8.imageset/Contents.json new file mode 100644 index 0000000000..99dcc301f9 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_hugo-98dcf69e-570a-4abf-a227-22704dfa10b8.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-4_images_2_types-oscar_hugo-98dcf69e-570a-4abf-a227-22704dfa10b8.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_hugo-98dcf69e-570a-4abf-a227-22704dfa10b8.imageset/emotion_recognition-drawings-4_images_2_types-oscar_hugo-98dcf69e-570a-4abf-a227-22704dfa10b8.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_hugo-98dcf69e-570a-4abf-a227-22704dfa10b8.imageset/emotion_recognition-drawings-4_images_2_types-oscar_hugo-98dcf69e-570a-4abf-a227-22704dfa10b8.pdf new file mode 100644 index 0000000000..0e280743c2 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_hugo-98dcf69e-570a-4abf-a227-22704dfa10b8.imageset/emotion_recognition-drawings-4_images_2_types-oscar_hugo-98dcf69e-570a-4abf-a227-22704dfa10b8.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_louna-8edf633c-18e5-4a89-b510-d0c37ff45c76.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_louna-8edf633c-18e5-4a89-b510-d0c37ff45c76.imageset/Contents.json new file mode 100644 index 0000000000..9a498d0049 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_louna-8edf633c-18e5-4a89-b510-d0c37ff45c76.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-4_images_2_types-oscar_louna-8edf633c-18e5-4a89-b510-d0c37ff45c76.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_louna-8edf633c-18e5-4a89-b510-d0c37ff45c76.imageset/emotion_recognition-drawings-4_images_2_types-oscar_louna-8edf633c-18e5-4a89-b510-d0c37ff45c76.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_louna-8edf633c-18e5-4a89-b510-d0c37ff45c76.imageset/emotion_recognition-drawings-4_images_2_types-oscar_louna-8edf633c-18e5-4a89-b510-d0c37ff45c76.pdf new file mode 100644 index 0000000000..b3681df1b5 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_louna-8edf633c-18e5-4a89-b510-d0c37ff45c76.imageset/emotion_recognition-drawings-4_images_2_types-oscar_louna-8edf633c-18e5-4a89-b510-d0c37ff45c76.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_moussa-2a3657f0-a247-4a9a-b0c7-0039a9d4da70.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_moussa-2a3657f0-a247-4a9a-b0c7-0039a9d4da70.imageset/Contents.json new file mode 100644 index 0000000000..e49640a8fe --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_moussa-2a3657f0-a247-4a9a-b0c7-0039a9d4da70.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-4_images_2_types-oscar_moussa-2a3657f0-a247-4a9a-b0c7-0039a9d4da70.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_moussa-2a3657f0-a247-4a9a-b0c7-0039a9d4da70.imageset/emotion_recognition-drawings-4_images_2_types-oscar_moussa-2a3657f0-a247-4a9a-b0c7-0039a9d4da70.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_moussa-2a3657f0-a247-4a9a-b0c7-0039a9d4da70.imageset/emotion_recognition-drawings-4_images_2_types-oscar_moussa-2a3657f0-a247-4a9a-b0c7-0039a9d4da70.pdf new file mode 100644 index 0000000000..d88d4c063e Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-oscar_moussa-2a3657f0-a247-4a9a-b0c7-0039a9d4da70.imageset/emotion_recognition-drawings-4_images_2_types-oscar_moussa-2a3657f0-a247-4a9a-b0c7-0039a9d4da70.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-zoe_hugo-75a91020-efc0-4350-8b79-3371489fa764.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-zoe_hugo-75a91020-efc0-4350-8b79-3371489fa764.imageset/Contents.json new file mode 100644 index 0000000000..7213350b99 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-zoe_hugo-75a91020-efc0-4350-8b79-3371489fa764.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-4_images_2_types-zoe_hugo-75a91020-efc0-4350-8b79-3371489fa764.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-zoe_hugo-75a91020-efc0-4350-8b79-3371489fa764.imageset/emotion_recognition-drawings-4_images_2_types-zoe_hugo-75a91020-efc0-4350-8b79-3371489fa764.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-zoe_hugo-75a91020-efc0-4350-8b79-3371489fa764.imageset/emotion_recognition-drawings-4_images_2_types-zoe_hugo-75a91020-efc0-4350-8b79-3371489fa764.pdf new file mode 100644 index 0000000000..15ed0769da Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-zoe_hugo-75a91020-efc0-4350-8b79-3371489fa764.imageset/emotion_recognition-drawings-4_images_2_types-zoe_hugo-75a91020-efc0-4350-8b79-3371489fa764.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-zoe_louna-2895687a-0515-4936-b60f-9e4b9669af95.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-zoe_louna-2895687a-0515-4936-b60f-9e4b9669af95.imageset/Contents.json new file mode 100644 index 0000000000..0d08a327f0 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-zoe_louna-2895687a-0515-4936-b60f-9e4b9669af95.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-4_images_2_types-zoe_louna-2895687a-0515-4936-b60f-9e4b9669af95.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-zoe_louna-2895687a-0515-4936-b60f-9e4b9669af95.imageset/emotion_recognition-drawings-4_images_2_types-zoe_louna-2895687a-0515-4936-b60f-9e4b9669af95.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-zoe_louna-2895687a-0515-4936-b60f-9e4b9669af95.imageset/emotion_recognition-drawings-4_images_2_types-zoe_louna-2895687a-0515-4936-b60f-9e4b9669af95.pdf new file mode 100644 index 0000000000..3a8b2f4221 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_2_types-zoe_louna-2895687a-0515-4936-b60f-9e4b9669af95.imageset/emotion_recognition-drawings-4_images_2_types-zoe_louna-2895687a-0515-4936-b60f-9e4b9669af95.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_4_types-oscar_louna_moussa_zoe_hugo-9d4ab4af-a683-4f28-b8bd-727d65fa0bbd.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_4_types-oscar_louna_moussa_zoe_hugo-9d4ab4af-a683-4f28-b8bd-727d65fa0bbd.imageset/Contents.json new file mode 100644 index 0000000000..088cb4288c --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_4_types-oscar_louna_moussa_zoe_hugo-9d4ab4af-a683-4f28-b8bd-727d65fa0bbd.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-drawings-4_images_4_types-oscar_louna_moussa_zoe_hugo-9d4ab4af-a683-4f28-b8bd-727d65fa0bbd.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_4_types-oscar_louna_moussa_zoe_hugo-9d4ab4af-a683-4f28-b8bd-727d65fa0bbd.imageset/emotion_recognition-drawings-4_images_4_types-oscar_louna_moussa_zoe_hugo-9d4ab4af-a683-4f28-b8bd-727d65fa0bbd.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_4_types-oscar_louna_moussa_zoe_hugo-9d4ab4af-a683-4f28-b8bd-727d65fa0bbd.imageset/emotion_recognition-drawings-4_images_4_types-oscar_louna_moussa_zoe_hugo-9d4ab4af-a683-4f28-b8bd-727d65fa0bbd.pdf new file mode 100644 index 0000000000..05f3d786e7 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-drawings-4_images_4_types-oscar_louna_moussa_zoe_hugo-9d4ab4af-a683-4f28-b8bd-727d65fa0bbd.imageset/emotion_recognition-drawings-4_images_4_types-oscar_louna_moussa_zoe_hugo-9d4ab4af-a683-4f28-b8bd-727d65fa0bbd.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-3_images_2_types-c93c9465-4559-426d-961b-7074e63b4782.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-3_images_2_types-c93c9465-4559-426d-961b-7074e63b4782.imageset/Contents.json new file mode 100644 index 0000000000..023b86a5b5 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-3_images_2_types-c93c9465-4559-426d-961b-7074e63b4782.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-generalization-3_images_2_types-c93c9465-4559-426d-961b-7074e63b4782.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-3_images_2_types-c93c9465-4559-426d-961b-7074e63b4782.imageset/emotion_recognition-generalization-3_images_2_types-c93c9465-4559-426d-961b-7074e63b4782.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-3_images_2_types-c93c9465-4559-426d-961b-7074e63b4782.imageset/emotion_recognition-generalization-3_images_2_types-c93c9465-4559-426d-961b-7074e63b4782.pdf new file mode 100644 index 0000000000..7b132cfeaf Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-3_images_2_types-c93c9465-4559-426d-961b-7074e63b4782.imageset/emotion_recognition-generalization-3_images_2_types-c93c9465-4559-426d-961b-7074e63b4782.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-3_images_3_types-3875e8e0-97b6-4655-a455-4d5d62f97aa7.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-3_images_3_types-3875e8e0-97b6-4655-a455-4d5d62f97aa7.imageset/Contents.json new file mode 100644 index 0000000000..c6639bd3d3 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-3_images_3_types-3875e8e0-97b6-4655-a455-4d5d62f97aa7.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-generalization-3_images_3_types-3875e8e0-97b6-4655-a455-4d5d62f97aa7.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-3_images_3_types-3875e8e0-97b6-4655-a455-4d5d62f97aa7.imageset/emotion_recognition-generalization-3_images_3_types-3875e8e0-97b6-4655-a455-4d5d62f97aa7.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-3_images_3_types-3875e8e0-97b6-4655-a455-4d5d62f97aa7.imageset/emotion_recognition-generalization-3_images_3_types-3875e8e0-97b6-4655-a455-4d5d62f97aa7.pdf new file mode 100644 index 0000000000..57b81e670f Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-3_images_3_types-3875e8e0-97b6-4655-a455-4d5d62f97aa7.imageset/emotion_recognition-generalization-3_images_3_types-3875e8e0-97b6-4655-a455-4d5d62f97aa7.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-4_images_2_types-7c083f94-cc67-418c-8ed2-2b2da3102612.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-4_images_2_types-7c083f94-cc67-418c-8ed2-2b2da3102612.imageset/Contents.json new file mode 100644 index 0000000000..42a61b8b4a --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-4_images_2_types-7c083f94-cc67-418c-8ed2-2b2da3102612.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-generalization-4_images_2_types-7c083f94-cc67-418c-8ed2-2b2da3102612.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-4_images_2_types-7c083f94-cc67-418c-8ed2-2b2da3102612.imageset/emotion_recognition-generalization-4_images_2_types-7c083f94-cc67-418c-8ed2-2b2da3102612.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-4_images_2_types-7c083f94-cc67-418c-8ed2-2b2da3102612.imageset/emotion_recognition-generalization-4_images_2_types-7c083f94-cc67-418c-8ed2-2b2da3102612.pdf new file mode 100644 index 0000000000..3e58498d31 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-4_images_2_types-7c083f94-cc67-418c-8ed2-2b2da3102612.imageset/emotion_recognition-generalization-4_images_2_types-7c083f94-cc67-418c-8ed2-2b2da3102612.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-4_images_3_types-0903663b-d062-434a-aa02-ad139509bd46.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-4_images_3_types-0903663b-d062-434a-aa02-ad139509bd46.imageset/Contents.json new file mode 100644 index 0000000000..2a6750a3a8 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-4_images_3_types-0903663b-d062-434a-aa02-ad139509bd46.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-generalization-4_images_3_types-0903663b-d062-434a-aa02-ad139509bd46.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-4_images_3_types-0903663b-d062-434a-aa02-ad139509bd46.imageset/emotion_recognition-generalization-4_images_3_types-0903663b-d062-434a-aa02-ad139509bd46.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-4_images_3_types-0903663b-d062-434a-aa02-ad139509bd46.imageset/emotion_recognition-generalization-4_images_3_types-0903663b-d062-434a-aa02-ad139509bd46.pdf new file mode 100644 index 0000000000..e4e0df6083 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-4_images_3_types-0903663b-d062-434a-aa02-ad139509bd46.imageset/emotion_recognition-generalization-4_images_3_types-0903663b-d062-434a-aa02-ad139509bd46.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-6_images_2_types-b7f10e00-5d59-48fa-8c30-fa9bbd3c738a.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-6_images_2_types-b7f10e00-5d59-48fa-8c30-fa9bbd3c738a.imageset/Contents.json new file mode 100644 index 0000000000..a34cbb7913 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-6_images_2_types-b7f10e00-5d59-48fa-8c30-fa9bbd3c738a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-generalization-6_images_2_types-b7f10e00-5d59-48fa-8c30-fa9bbd3c738a.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-6_images_2_types-b7f10e00-5d59-48fa-8c30-fa9bbd3c738a.imageset/emotion_recognition-generalization-6_images_2_types-b7f10e00-5d59-48fa-8c30-fa9bbd3c738a.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-6_images_2_types-b7f10e00-5d59-48fa-8c30-fa9bbd3c738a.imageset/emotion_recognition-generalization-6_images_2_types-b7f10e00-5d59-48fa-8c30-fa9bbd3c738a.pdf new file mode 100644 index 0000000000..85f8a7c04b Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-6_images_2_types-b7f10e00-5d59-48fa-8c30-fa9bbd3c738a.imageset/emotion_recognition-generalization-6_images_2_types-b7f10e00-5d59-48fa-8c30-fa9bbd3c738a.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-6_images_3_types-46511f38-10ec-4f0a-b84f-18bc8dd876b5.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-6_images_3_types-46511f38-10ec-4f0a-b84f-18bc8dd876b5.imageset/Contents.json new file mode 100644 index 0000000000..5982157864 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-6_images_3_types-46511f38-10ec-4f0a-b84f-18bc8dd876b5.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-generalization-6_images_3_types-46511f38-10ec-4f0a-b84f-18bc8dd876b5.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-6_images_3_types-46511f38-10ec-4f0a-b84f-18bc8dd876b5.imageset/emotion_recognition-generalization-6_images_3_types-46511f38-10ec-4f0a-b84f-18bc8dd876b5.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-6_images_3_types-46511f38-10ec-4f0a-b84f-18bc8dd876b5.imageset/emotion_recognition-generalization-6_images_3_types-46511f38-10ec-4f0a-b84f-18bc8dd876b5.pdf new file mode 100644 index 0000000000..f416eefec3 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-generalization-6_images_3_types-46511f38-10ec-4f0a-b84f-18bc8dd876b5.imageset/emotion_recognition-generalization-6_images_3_types-46511f38-10ec-4f0a-b84f-18bc8dd876b5.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-1_images_1_types-leka-b7018cde-3d47-4914-a36f-54b531d0ad7b.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-1_images_1_types-leka-b7018cde-3d47-4914-a36f-54b531d0ad7b.imageset/Contents.json new file mode 100644 index 0000000000..928aadca0c --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-1_images_1_types-leka-b7018cde-3d47-4914-a36f-54b531d0ad7b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictograms-1_images_1_types-leka-b7018cde-3d47-4914-a36f-54b531d0ad7b.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-1_images_1_types-leka-b7018cde-3d47-4914-a36f-54b531d0ad7b.imageset/emotion_recognition-pictograms-1_images_1_types-leka-b7018cde-3d47-4914-a36f-54b531d0ad7b.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-1_images_1_types-leka-b7018cde-3d47-4914-a36f-54b531d0ad7b.imageset/emotion_recognition-pictograms-1_images_1_types-leka-b7018cde-3d47-4914-a36f-54b531d0ad7b.pdf new file mode 100644 index 0000000000..8f9ba2f720 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-1_images_1_types-leka-b7018cde-3d47-4914-a36f-54b531d0ad7b.imageset/emotion_recognition-pictograms-1_images_1_types-leka-b7018cde-3d47-4914-a36f-54b531d0ad7b.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-2_images_1_types-leka-6e66b4bf-3f07-4102-89aa-462f9709285a.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-2_images_1_types-leka-6e66b4bf-3f07-4102-89aa-462f9709285a.imageset/Contents.json new file mode 100644 index 0000000000..314d39452c --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-2_images_1_types-leka-6e66b4bf-3f07-4102-89aa-462f9709285a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictograms-2_images_1_types-leka-6e66b4bf-3f07-4102-89aa-462f9709285a.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-2_images_1_types-leka-6e66b4bf-3f07-4102-89aa-462f9709285a.imageset/emotion_recognition-pictograms-2_images_1_types-leka-6e66b4bf-3f07-4102-89aa-462f9709285a.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-2_images_1_types-leka-6e66b4bf-3f07-4102-89aa-462f9709285a.imageset/emotion_recognition-pictograms-2_images_1_types-leka-6e66b4bf-3f07-4102-89aa-462f9709285a.pdf new file mode 100644 index 0000000000..2214559042 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-2_images_1_types-leka-6e66b4bf-3f07-4102-89aa-462f9709285a.imageset/emotion_recognition-pictograms-2_images_1_types-leka-6e66b4bf-3f07-4102-89aa-462f9709285a.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-3_images_1_types-leka-c96eed6b-d65d-418d-b7c7-1b25ff9eb38d.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-3_images_1_types-leka-c96eed6b-d65d-418d-b7c7-1b25ff9eb38d.imageset/Contents.json new file mode 100644 index 0000000000..a597cfad81 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-3_images_1_types-leka-c96eed6b-d65d-418d-b7c7-1b25ff9eb38d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictograms-3_images_1_types-leka-c96eed6b-d65d-418d-b7c7-1b25ff9eb38d.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-3_images_1_types-leka-c96eed6b-d65d-418d-b7c7-1b25ff9eb38d.imageset/emotion_recognition-pictograms-3_images_1_types-leka-c96eed6b-d65d-418d-b7c7-1b25ff9eb38d.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-3_images_1_types-leka-c96eed6b-d65d-418d-b7c7-1b25ff9eb38d.imageset/emotion_recognition-pictograms-3_images_1_types-leka-c96eed6b-d65d-418d-b7c7-1b25ff9eb38d.pdf new file mode 100644 index 0000000000..73b36932d2 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-3_images_1_types-leka-c96eed6b-d65d-418d-b7c7-1b25ff9eb38d.imageset/emotion_recognition-pictograms-3_images_1_types-leka-c96eed6b-d65d-418d-b7c7-1b25ff9eb38d.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-4_images_1_types-leka-dfe16eaa-e149-4df1-873e-995dc1b1c5b0.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-4_images_1_types-leka-dfe16eaa-e149-4df1-873e-995dc1b1c5b0.imageset/Contents.json new file mode 100644 index 0000000000..630d3f292f --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-4_images_1_types-leka-dfe16eaa-e149-4df1-873e-995dc1b1c5b0.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictograms-4_images_1_types-leka-dfe16eaa-e149-4df1-873e-995dc1b1c5b0.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-4_images_1_types-leka-dfe16eaa-e149-4df1-873e-995dc1b1c5b0.imageset/emotion_recognition-pictograms-4_images_1_types-leka-dfe16eaa-e149-4df1-873e-995dc1b1c5b0.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-4_images_1_types-leka-dfe16eaa-e149-4df1-873e-995dc1b1c5b0.imageset/emotion_recognition-pictograms-4_images_1_types-leka-dfe16eaa-e149-4df1-873e-995dc1b1c5b0.pdf new file mode 100644 index 0000000000..377abb196b Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictograms-4_images_1_types-leka-dfe16eaa-e149-4df1-873e-995dc1b1c5b0.imageset/emotion_recognition-pictograms-4_images_1_types-leka-dfe16eaa-e149-4df1-873e-995dc1b1c5b0.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-hortense-195c7905-0188-4c1d-a3c9-0aed0199606d.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-hortense-195c7905-0188-4c1d-a3c9-0aed0199606d.imageset/Contents.json new file mode 100644 index 0000000000..5fba59b6be --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-hortense-195c7905-0188-4c1d-a3c9-0aed0199606d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-1_images_1_types-hortense-195c7905-0188-4c1d-a3c9-0aed0199606d.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-hortense-195c7905-0188-4c1d-a3c9-0aed0199606d.imageset/emotion_recognition-pictures-1_images_1_types-hortense-195c7905-0188-4c1d-a3c9-0aed0199606d.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-hortense-195c7905-0188-4c1d-a3c9-0aed0199606d.imageset/emotion_recognition-pictures-1_images_1_types-hortense-195c7905-0188-4c1d-a3c9-0aed0199606d.pdf new file mode 100644 index 0000000000..1f0ca6f68c Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-hortense-195c7905-0188-4c1d-a3c9-0aed0199606d.imageset/emotion_recognition-pictures-1_images_1_types-hortense-195c7905-0188-4c1d-a3c9-0aed0199606d.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-hugo-30e86d7d-b937-4721-9e5a-3a0d1e8a1ace.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-hugo-30e86d7d-b937-4721-9e5a-3a0d1e8a1ace.imageset/Contents.json new file mode 100644 index 0000000000..3b011a7e6e --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-hugo-30e86d7d-b937-4721-9e5a-3a0d1e8a1ace.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-1_images_1_types-hugo-30e86d7d-b937-4721-9e5a-3a0d1e8a1ace.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-hugo-30e86d7d-b937-4721-9e5a-3a0d1e8a1ace.imageset/emotion_recognition-pictures-1_images_1_types-hugo-30e86d7d-b937-4721-9e5a-3a0d1e8a1ace.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-hugo-30e86d7d-b937-4721-9e5a-3a0d1e8a1ace.imageset/emotion_recognition-pictures-1_images_1_types-hugo-30e86d7d-b937-4721-9e5a-3a0d1e8a1ace.pdf new file mode 100644 index 0000000000..18bc37057e Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-hugo-30e86d7d-b937-4721-9e5a-3a0d1e8a1ace.imageset/emotion_recognition-pictures-1_images_1_types-hugo-30e86d7d-b937-4721-9e5a-3a0d1e8a1ace.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-ladislas-ddc5380e-efc6-4abf-b03b-603b73012e7b.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-ladislas-ddc5380e-efc6-4abf-b03b-603b73012e7b.imageset/Contents.json new file mode 100644 index 0000000000..ffe06c985e --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-ladislas-ddc5380e-efc6-4abf-b03b-603b73012e7b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-1_images_1_types-ladislas-ddc5380e-efc6-4abf-b03b-603b73012e7b.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-ladislas-ddc5380e-efc6-4abf-b03b-603b73012e7b.imageset/emotion_recognition-pictures-1_images_1_types-ladislas-ddc5380e-efc6-4abf-b03b-603b73012e7b.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-ladislas-ddc5380e-efc6-4abf-b03b-603b73012e7b.imageset/emotion_recognition-pictures-1_images_1_types-ladislas-ddc5380e-efc6-4abf-b03b-603b73012e7b.pdf new file mode 100644 index 0000000000..cace997f5d Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-ladislas-ddc5380e-efc6-4abf-b03b-603b73012e7b.imageset/emotion_recognition-pictures-1_images_1_types-ladislas-ddc5380e-efc6-4abf-b03b-603b73012e7b.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-lucie-3fc7b66e-67a0-4f50-9817-9ef19af7f717.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-lucie-3fc7b66e-67a0-4f50-9817-9ef19af7f717.imageset/Contents.json new file mode 100644 index 0000000000..9bf85d00e6 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-lucie-3fc7b66e-67a0-4f50-9817-9ef19af7f717.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-1_images_1_types-lucie-3fc7b66e-67a0-4f50-9817-9ef19af7f717.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-lucie-3fc7b66e-67a0-4f50-9817-9ef19af7f717.imageset/emotion_recognition-pictures-1_images_1_types-lucie-3fc7b66e-67a0-4f50-9817-9ef19af7f717.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-lucie-3fc7b66e-67a0-4f50-9817-9ef19af7f717.imageset/emotion_recognition-pictures-1_images_1_types-lucie-3fc7b66e-67a0-4f50-9817-9ef19af7f717.pdf new file mode 100644 index 0000000000..cafc9c139d Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-lucie-3fc7b66e-67a0-4f50-9817-9ef19af7f717.imageset/emotion_recognition-pictures-1_images_1_types-lucie-3fc7b66e-67a0-4f50-9817-9ef19af7f717.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-yann-c44a02c1-7ae8-45f8-b6f5-58d39e4e363b.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-yann-c44a02c1-7ae8-45f8-b6f5-58d39e4e363b.imageset/Contents.json new file mode 100644 index 0000000000..d4d4c901c6 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-yann-c44a02c1-7ae8-45f8-b6f5-58d39e4e363b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-1_images_1_types-yann-c44a02c1-7ae8-45f8-b6f5-58d39e4e363b.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-yann-c44a02c1-7ae8-45f8-b6f5-58d39e4e363b.imageset/emotion_recognition-pictures-1_images_1_types-yann-c44a02c1-7ae8-45f8-b6f5-58d39e4e363b.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-yann-c44a02c1-7ae8-45f8-b6f5-58d39e4e363b.imageset/emotion_recognition-pictures-1_images_1_types-yann-c44a02c1-7ae8-45f8-b6f5-58d39e4e363b.pdf new file mode 100644 index 0000000000..7a146de008 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-1_images_1_types-yann-c44a02c1-7ae8-45f8-b6f5-58d39e4e363b.imageset/emotion_recognition-pictures-1_images_1_types-yann-c44a02c1-7ae8-45f8-b6f5-58d39e4e363b.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-hortense-b88f7a94-9e41-4dc6-b8ef-dbc50543e976.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-hortense-b88f7a94-9e41-4dc6-b8ef-dbc50543e976.imageset/Contents.json new file mode 100644 index 0000000000..2028feaee7 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-hortense-b88f7a94-9e41-4dc6-b8ef-dbc50543e976.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-2_images_1_types-hortense-b88f7a94-9e41-4dc6-b8ef-dbc50543e976.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-hortense-b88f7a94-9e41-4dc6-b8ef-dbc50543e976.imageset/emotion_recognition-pictures-2_images_1_types-hortense-b88f7a94-9e41-4dc6-b8ef-dbc50543e976.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-hortense-b88f7a94-9e41-4dc6-b8ef-dbc50543e976.imageset/emotion_recognition-pictures-2_images_1_types-hortense-b88f7a94-9e41-4dc6-b8ef-dbc50543e976.pdf new file mode 100644 index 0000000000..dd9e00fec9 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-hortense-b88f7a94-9e41-4dc6-b8ef-dbc50543e976.imageset/emotion_recognition-pictures-2_images_1_types-hortense-b88f7a94-9e41-4dc6-b8ef-dbc50543e976.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-hugo-0c0ab0f5-ed2f-468f-bb3d-e3bf8b9512c5.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-hugo-0c0ab0f5-ed2f-468f-bb3d-e3bf8b9512c5.imageset/Contents.json new file mode 100644 index 0000000000..3dcab7dc0c --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-hugo-0c0ab0f5-ed2f-468f-bb3d-e3bf8b9512c5.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-2_images_1_types-hugo-0c0ab0f5-ed2f-468f-bb3d-e3bf8b9512c5.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-hugo-0c0ab0f5-ed2f-468f-bb3d-e3bf8b9512c5.imageset/emotion_recognition-pictures-2_images_1_types-hugo-0c0ab0f5-ed2f-468f-bb3d-e3bf8b9512c5.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-hugo-0c0ab0f5-ed2f-468f-bb3d-e3bf8b9512c5.imageset/emotion_recognition-pictures-2_images_1_types-hugo-0c0ab0f5-ed2f-468f-bb3d-e3bf8b9512c5.pdf new file mode 100644 index 0000000000..f41b75c50d Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-hugo-0c0ab0f5-ed2f-468f-bb3d-e3bf8b9512c5.imageset/emotion_recognition-pictures-2_images_1_types-hugo-0c0ab0f5-ed2f-468f-bb3d-e3bf8b9512c5.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-ladislas-a8590b9f-cd04-447d-a76d-1c2d99ce2615.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-ladislas-a8590b9f-cd04-447d-a76d-1c2d99ce2615.imageset/Contents.json new file mode 100644 index 0000000000..e45358a031 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-ladislas-a8590b9f-cd04-447d-a76d-1c2d99ce2615.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-2_images_1_types-ladislas-a8590b9f-cd04-447d-a76d-1c2d99ce2615.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-ladislas-a8590b9f-cd04-447d-a76d-1c2d99ce2615.imageset/emotion_recognition-pictures-2_images_1_types-ladislas-a8590b9f-cd04-447d-a76d-1c2d99ce2615.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-ladislas-a8590b9f-cd04-447d-a76d-1c2d99ce2615.imageset/emotion_recognition-pictures-2_images_1_types-ladislas-a8590b9f-cd04-447d-a76d-1c2d99ce2615.pdf new file mode 100644 index 0000000000..afc51fcd5c Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-ladislas-a8590b9f-cd04-447d-a76d-1c2d99ce2615.imageset/emotion_recognition-pictures-2_images_1_types-ladislas-a8590b9f-cd04-447d-a76d-1c2d99ce2615.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-lucie-7d6c0af7-9bc4-434b-b07e-0761d0a4b39e.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-lucie-7d6c0af7-9bc4-434b-b07e-0761d0a4b39e.imageset/Contents.json new file mode 100644 index 0000000000..a0d33d4276 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-lucie-7d6c0af7-9bc4-434b-b07e-0761d0a4b39e.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-2_images_1_types-lucie-7d6c0af7-9bc4-434b-b07e-0761d0a4b39e.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-lucie-7d6c0af7-9bc4-434b-b07e-0761d0a4b39e.imageset/emotion_recognition-pictures-2_images_1_types-lucie-7d6c0af7-9bc4-434b-b07e-0761d0a4b39e.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-lucie-7d6c0af7-9bc4-434b-b07e-0761d0a4b39e.imageset/emotion_recognition-pictures-2_images_1_types-lucie-7d6c0af7-9bc4-434b-b07e-0761d0a4b39e.pdf new file mode 100644 index 0000000000..afbaf5ff03 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-lucie-7d6c0af7-9bc4-434b-b07e-0761d0a4b39e.imageset/emotion_recognition-pictures-2_images_1_types-lucie-7d6c0af7-9bc4-434b-b07e-0761d0a4b39e.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-yann-3873d0ba-e98d-44ca-bdf9-b7695e58648e.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-yann-3873d0ba-e98d-44ca-bdf9-b7695e58648e.imageset/Contents.json new file mode 100644 index 0000000000..d23e13a0e2 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-yann-3873d0ba-e98d-44ca-bdf9-b7695e58648e.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-2_images_1_types-yann-3873d0ba-e98d-44ca-bdf9-b7695e58648e.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-yann-3873d0ba-e98d-44ca-bdf9-b7695e58648e.imageset/emotion_recognition-pictures-2_images_1_types-yann-3873d0ba-e98d-44ca-bdf9-b7695e58648e.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-yann-3873d0ba-e98d-44ca-bdf9-b7695e58648e.imageset/emotion_recognition-pictures-2_images_1_types-yann-3873d0ba-e98d-44ca-bdf9-b7695e58648e.pdf new file mode 100644 index 0000000000..101b972d1a Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_1_types-yann-3873d0ba-e98d-44ca-bdf9-b7695e58648e.imageset/emotion_recognition-pictures-2_images_1_types-yann-3873d0ba-e98d-44ca-bdf9-b7695e58648e.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-hortense_lucie-01cd2e22-846d-4873-95d5-414366ef0cd5.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-hortense_lucie-01cd2e22-846d-4873-95d5-414366ef0cd5.imageset/Contents.json new file mode 100644 index 0000000000..4cd82d5a31 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-hortense_lucie-01cd2e22-846d-4873-95d5-414366ef0cd5.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-2_images_2_types-hortense_lucie-01cd2e22-846d-4873-95d5-414366ef0cd5.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-hortense_lucie-01cd2e22-846d-4873-95d5-414366ef0cd5.imageset/emotion_recognition-pictures-2_images_2_types-hortense_lucie-01cd2e22-846d-4873-95d5-414366ef0cd5.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-hortense_lucie-01cd2e22-846d-4873-95d5-414366ef0cd5.imageset/emotion_recognition-pictures-2_images_2_types-hortense_lucie-01cd2e22-846d-4873-95d5-414366ef0cd5.pdf new file mode 100644 index 0000000000..145e2a7e50 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-hortense_lucie-01cd2e22-846d-4873-95d5-414366ef0cd5.imageset/emotion_recognition-pictures-2_images_2_types-hortense_lucie-01cd2e22-846d-4873-95d5-414366ef0cd5.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-hortense_yann-ac80e69e-caba-41f6-8001-c359870b8569.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-hortense_yann-ac80e69e-caba-41f6-8001-c359870b8569.imageset/Contents.json new file mode 100644 index 0000000000..681097b047 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-hortense_yann-ac80e69e-caba-41f6-8001-c359870b8569.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-2_images_2_types-hortense_yann-ac80e69e-caba-41f6-8001-c359870b8569.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-hortense_yann-ac80e69e-caba-41f6-8001-c359870b8569.imageset/emotion_recognition-pictures-2_images_2_types-hortense_yann-ac80e69e-caba-41f6-8001-c359870b8569.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-hortense_yann-ac80e69e-caba-41f6-8001-c359870b8569.imageset/emotion_recognition-pictures-2_images_2_types-hortense_yann-ac80e69e-caba-41f6-8001-c359870b8569.pdf new file mode 100644 index 0000000000..c2dd90ff12 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-hortense_yann-ac80e69e-caba-41f6-8001-c359870b8569.imageset/emotion_recognition-pictures-2_images_2_types-hortense_yann-ac80e69e-caba-41f6-8001-c359870b8569.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-ladislas_hugo-4246413b-2fe4-4b53-aa0b-3b170f053eda.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-ladislas_hugo-4246413b-2fe4-4b53-aa0b-3b170f053eda.imageset/Contents.json new file mode 100644 index 0000000000..21192a9970 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-ladislas_hugo-4246413b-2fe4-4b53-aa0b-3b170f053eda.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-2_images_2_types-ladislas_hugo-4246413b-2fe4-4b53-aa0b-3b170f053eda.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-ladislas_hugo-4246413b-2fe4-4b53-aa0b-3b170f053eda.imageset/emotion_recognition-pictures-2_images_2_types-ladislas_hugo-4246413b-2fe4-4b53-aa0b-3b170f053eda.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-ladislas_hugo-4246413b-2fe4-4b53-aa0b-3b170f053eda.imageset/emotion_recognition-pictures-2_images_2_types-ladislas_hugo-4246413b-2fe4-4b53-aa0b-3b170f053eda.pdf new file mode 100644 index 0000000000..396ac7ef82 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-ladislas_hugo-4246413b-2fe4-4b53-aa0b-3b170f053eda.imageset/emotion_recognition-pictures-2_images_2_types-ladislas_hugo-4246413b-2fe4-4b53-aa0b-3b170f053eda.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-lucie_ladislas-b9988679-9d34-49ff-bda9-0802fe83efa9.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-lucie_ladislas-b9988679-9d34-49ff-bda9-0802fe83efa9.imageset/Contents.json new file mode 100644 index 0000000000..5a8de81d7f --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-lucie_ladislas-b9988679-9d34-49ff-bda9-0802fe83efa9.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-2_images_2_types-lucie_ladislas-b9988679-9d34-49ff-bda9-0802fe83efa9.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-lucie_ladislas-b9988679-9d34-49ff-bda9-0802fe83efa9.imageset/emotion_recognition-pictures-2_images_2_types-lucie_ladislas-b9988679-9d34-49ff-bda9-0802fe83efa9.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-lucie_ladislas-b9988679-9d34-49ff-bda9-0802fe83efa9.imageset/emotion_recognition-pictures-2_images_2_types-lucie_ladislas-b9988679-9d34-49ff-bda9-0802fe83efa9.pdf new file mode 100644 index 0000000000..84ce22ba0c Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-lucie_ladislas-b9988679-9d34-49ff-bda9-0802fe83efa9.imageset/emotion_recognition-pictures-2_images_2_types-lucie_ladislas-b9988679-9d34-49ff-bda9-0802fe83efa9.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-yann_hugo-4abe3280-bdb4-4b5b-b3f2-1dd8cddecdd7.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-yann_hugo-4abe3280-bdb4-4b5b-b3f2-1dd8cddecdd7.imageset/Contents.json new file mode 100644 index 0000000000..a057a534e0 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-yann_hugo-4abe3280-bdb4-4b5b-b3f2-1dd8cddecdd7.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-2_images_2_types-yann_hugo-4abe3280-bdb4-4b5b-b3f2-1dd8cddecdd7.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-yann_hugo-4abe3280-bdb4-4b5b-b3f2-1dd8cddecdd7.imageset/emotion_recognition-pictures-2_images_2_types-yann_hugo-4abe3280-bdb4-4b5b-b3f2-1dd8cddecdd7.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-yann_hugo-4abe3280-bdb4-4b5b-b3f2-1dd8cddecdd7.imageset/emotion_recognition-pictures-2_images_2_types-yann_hugo-4abe3280-bdb4-4b5b-b3f2-1dd8cddecdd7.pdf new file mode 100644 index 0000000000..87dff3d541 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-2_images_2_types-yann_hugo-4abe3280-bdb4-4b5b-b3f2-1dd8cddecdd7.imageset/emotion_recognition-pictures-2_images_2_types-yann_hugo-4abe3280-bdb4-4b5b-b3f2-1dd8cddecdd7.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-hortense-e64a56b0-0481-4688-b285-80fcf3020bba.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-hortense-e64a56b0-0481-4688-b285-80fcf3020bba.imageset/Contents.json new file mode 100644 index 0000000000..4a43164afa --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-hortense-e64a56b0-0481-4688-b285-80fcf3020bba.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-4_images_1_types-hortense-e64a56b0-0481-4688-b285-80fcf3020bba.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-hortense-e64a56b0-0481-4688-b285-80fcf3020bba.imageset/emotion_recognition-pictures-4_images_1_types-hortense-e64a56b0-0481-4688-b285-80fcf3020bba.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-hortense-e64a56b0-0481-4688-b285-80fcf3020bba.imageset/emotion_recognition-pictures-4_images_1_types-hortense-e64a56b0-0481-4688-b285-80fcf3020bba.pdf new file mode 100644 index 0000000000..8893478b2e Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-hortense-e64a56b0-0481-4688-b285-80fcf3020bba.imageset/emotion_recognition-pictures-4_images_1_types-hortense-e64a56b0-0481-4688-b285-80fcf3020bba.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-hugo-ac9c2620-9fba-4a60-aa39-8e5fbdc57d7b.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-hugo-ac9c2620-9fba-4a60-aa39-8e5fbdc57d7b.imageset/Contents.json new file mode 100644 index 0000000000..379e16d1e4 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-hugo-ac9c2620-9fba-4a60-aa39-8e5fbdc57d7b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-4_images_1_types-hugo-ac9c2620-9fba-4a60-aa39-8e5fbdc57d7b.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-hugo-ac9c2620-9fba-4a60-aa39-8e5fbdc57d7b.imageset/emotion_recognition-pictures-4_images_1_types-hugo-ac9c2620-9fba-4a60-aa39-8e5fbdc57d7b.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-hugo-ac9c2620-9fba-4a60-aa39-8e5fbdc57d7b.imageset/emotion_recognition-pictures-4_images_1_types-hugo-ac9c2620-9fba-4a60-aa39-8e5fbdc57d7b.pdf new file mode 100644 index 0000000000..8daec6dfbd Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-hugo-ac9c2620-9fba-4a60-aa39-8e5fbdc57d7b.imageset/emotion_recognition-pictures-4_images_1_types-hugo-ac9c2620-9fba-4a60-aa39-8e5fbdc57d7b.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-ladislas-7c8cc8cb-c6ce-4fed-b656-d98018ad2502.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-ladislas-7c8cc8cb-c6ce-4fed-b656-d98018ad2502.imageset/Contents.json new file mode 100644 index 0000000000..b3fe3eb6e7 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-ladislas-7c8cc8cb-c6ce-4fed-b656-d98018ad2502.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-4_images_1_types-ladislas-7c8cc8cb-c6ce-4fed-b656-d98018ad2502.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-ladislas-7c8cc8cb-c6ce-4fed-b656-d98018ad2502.imageset/emotion_recognition-pictures-4_images_1_types-ladislas-7c8cc8cb-c6ce-4fed-b656-d98018ad2502.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-ladislas-7c8cc8cb-c6ce-4fed-b656-d98018ad2502.imageset/emotion_recognition-pictures-4_images_1_types-ladislas-7c8cc8cb-c6ce-4fed-b656-d98018ad2502.pdf new file mode 100644 index 0000000000..1409565269 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-ladislas-7c8cc8cb-c6ce-4fed-b656-d98018ad2502.imageset/emotion_recognition-pictures-4_images_1_types-ladislas-7c8cc8cb-c6ce-4fed-b656-d98018ad2502.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-ladislas_lucie_yann_hortense_hugo-46042e54-7d41-4c4c-a40a-0dad3b51b5b0.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-ladislas_lucie_yann_hortense_hugo-46042e54-7d41-4c4c-a40a-0dad3b51b5b0.imageset/Contents.json new file mode 100644 index 0000000000..7255742155 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-ladislas_lucie_yann_hortense_hugo-46042e54-7d41-4c4c-a40a-0dad3b51b5b0.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-4_images_1_types-ladislas_lucie_yann_hortense_hugo-46042e54-7d41-4c4c-a40a-0dad3b51b5b0.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-ladislas_lucie_yann_hortense_hugo-46042e54-7d41-4c4c-a40a-0dad3b51b5b0.imageset/emotion_recognition-pictures-4_images_1_types-ladislas_lucie_yann_hortense_hugo-46042e54-7d41-4c4c-a40a-0dad3b51b5b0.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-ladislas_lucie_yann_hortense_hugo-46042e54-7d41-4c4c-a40a-0dad3b51b5b0.imageset/emotion_recognition-pictures-4_images_1_types-ladislas_lucie_yann_hortense_hugo-46042e54-7d41-4c4c-a40a-0dad3b51b5b0.pdf new file mode 100644 index 0000000000..b13b4b0a01 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-ladislas_lucie_yann_hortense_hugo-46042e54-7d41-4c4c-a40a-0dad3b51b5b0.imageset/emotion_recognition-pictures-4_images_1_types-ladislas_lucie_yann_hortense_hugo-46042e54-7d41-4c4c-a40a-0dad3b51b5b0.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-lucie-540372bf-1e0d-4969-a49c-61d0bfb7b579.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-lucie-540372bf-1e0d-4969-a49c-61d0bfb7b579.imageset/Contents.json new file mode 100644 index 0000000000..db72f83bd4 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-lucie-540372bf-1e0d-4969-a49c-61d0bfb7b579.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-4_images_1_types-lucie-540372bf-1e0d-4969-a49c-61d0bfb7b579.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-lucie-540372bf-1e0d-4969-a49c-61d0bfb7b579.imageset/emotion_recognition-pictures-4_images_1_types-lucie-540372bf-1e0d-4969-a49c-61d0bfb7b579.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-lucie-540372bf-1e0d-4969-a49c-61d0bfb7b579.imageset/emotion_recognition-pictures-4_images_1_types-lucie-540372bf-1e0d-4969-a49c-61d0bfb7b579.pdf new file mode 100644 index 0000000000..27d480a1f6 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-lucie-540372bf-1e0d-4969-a49c-61d0bfb7b579.imageset/emotion_recognition-pictures-4_images_1_types-lucie-540372bf-1e0d-4969-a49c-61d0bfb7b579.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-yann-1b1cce01-b360-4552-833e-01e05d476008.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-yann-1b1cce01-b360-4552-833e-01e05d476008.imageset/Contents.json new file mode 100644 index 0000000000..f667ca64c3 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-yann-1b1cce01-b360-4552-833e-01e05d476008.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-4_images_1_types-yann-1b1cce01-b360-4552-833e-01e05d476008.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-yann-1b1cce01-b360-4552-833e-01e05d476008.imageset/emotion_recognition-pictures-4_images_1_types-yann-1b1cce01-b360-4552-833e-01e05d476008.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-yann-1b1cce01-b360-4552-833e-01e05d476008.imageset/emotion_recognition-pictures-4_images_1_types-yann-1b1cce01-b360-4552-833e-01e05d476008.pdf new file mode 100644 index 0000000000..37f8439d7c Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_1_types-yann-1b1cce01-b360-4552-833e-01e05d476008.imageset/emotion_recognition-pictures-4_images_1_types-yann-1b1cce01-b360-4552-833e-01e05d476008.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hortense_lucie-721a6030-75f0-4731-8ded-2f1a5ba06e69.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hortense_lucie-721a6030-75f0-4731-8ded-2f1a5ba06e69.imageset/Contents.json new file mode 100644 index 0000000000..834eddf710 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hortense_lucie-721a6030-75f0-4731-8ded-2f1a5ba06e69.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-4_images_2_types-hortense_lucie-721a6030-75f0-4731-8ded-2f1a5ba06e69.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hortense_lucie-721a6030-75f0-4731-8ded-2f1a5ba06e69.imageset/emotion_recognition-pictures-4_images_2_types-hortense_lucie-721a6030-75f0-4731-8ded-2f1a5ba06e69.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hortense_lucie-721a6030-75f0-4731-8ded-2f1a5ba06e69.imageset/emotion_recognition-pictures-4_images_2_types-hortense_lucie-721a6030-75f0-4731-8ded-2f1a5ba06e69.pdf new file mode 100644 index 0000000000..54ed171792 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hortense_lucie-721a6030-75f0-4731-8ded-2f1a5ba06e69.imageset/emotion_recognition-pictures-4_images_2_types-hortense_lucie-721a6030-75f0-4731-8ded-2f1a5ba06e69.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hugo_ladislas-97938c8e-6f21-4fff-a24e-a51ae46baba6.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hugo_ladislas-97938c8e-6f21-4fff-a24e-a51ae46baba6.imageset/Contents.json new file mode 100644 index 0000000000..927137b781 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hugo_ladislas-97938c8e-6f21-4fff-a24e-a51ae46baba6.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-4_images_2_types-hugo_ladislas-97938c8e-6f21-4fff-a24e-a51ae46baba6.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hugo_ladislas-97938c8e-6f21-4fff-a24e-a51ae46baba6.imageset/emotion_recognition-pictures-4_images_2_types-hugo_ladislas-97938c8e-6f21-4fff-a24e-a51ae46baba6.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hugo_ladislas-97938c8e-6f21-4fff-a24e-a51ae46baba6.imageset/emotion_recognition-pictures-4_images_2_types-hugo_ladislas-97938c8e-6f21-4fff-a24e-a51ae46baba6.pdf new file mode 100644 index 0000000000..c0fa4ca9c9 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hugo_ladislas-97938c8e-6f21-4fff-a24e-a51ae46baba6.imageset/emotion_recognition-pictures-4_images_2_types-hugo_ladislas-97938c8e-6f21-4fff-a24e-a51ae46baba6.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hugo_yann-969bc9f7-da53-41ab-ba3a-6b9c5e6773cf.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hugo_yann-969bc9f7-da53-41ab-ba3a-6b9c5e6773cf.imageset/Contents.json new file mode 100644 index 0000000000..2b0ea81f41 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hugo_yann-969bc9f7-da53-41ab-ba3a-6b9c5e6773cf.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-4_images_2_types-hugo_yann-969bc9f7-da53-41ab-ba3a-6b9c5e6773cf.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hugo_yann-969bc9f7-da53-41ab-ba3a-6b9c5e6773cf.imageset/emotion_recognition-pictures-4_images_2_types-hugo_yann-969bc9f7-da53-41ab-ba3a-6b9c5e6773cf.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hugo_yann-969bc9f7-da53-41ab-ba3a-6b9c5e6773cf.imageset/emotion_recognition-pictures-4_images_2_types-hugo_yann-969bc9f7-da53-41ab-ba3a-6b9c5e6773cf.pdf new file mode 100644 index 0000000000..6402883440 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-hugo_yann-969bc9f7-da53-41ab-ba3a-6b9c5e6773cf.imageset/emotion_recognition-pictures-4_images_2_types-hugo_yann-969bc9f7-da53-41ab-ba3a-6b9c5e6773cf.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-ladislas_lucie-53aab2b3-7310-4ba6-b2bd-979d738cd81d.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-ladislas_lucie-53aab2b3-7310-4ba6-b2bd-979d738cd81d.imageset/Contents.json new file mode 100644 index 0000000000..cad6d919c9 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-ladislas_lucie-53aab2b3-7310-4ba6-b2bd-979d738cd81d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-4_images_2_types-ladislas_lucie-53aab2b3-7310-4ba6-b2bd-979d738cd81d.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-ladislas_lucie-53aab2b3-7310-4ba6-b2bd-979d738cd81d.imageset/emotion_recognition-pictures-4_images_2_types-ladislas_lucie-53aab2b3-7310-4ba6-b2bd-979d738cd81d.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-ladislas_lucie-53aab2b3-7310-4ba6-b2bd-979d738cd81d.imageset/emotion_recognition-pictures-4_images_2_types-ladislas_lucie-53aab2b3-7310-4ba6-b2bd-979d738cd81d.pdf new file mode 100644 index 0000000000..19396d0b99 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-ladislas_lucie-53aab2b3-7310-4ba6-b2bd-979d738cd81d.imageset/emotion_recognition-pictures-4_images_2_types-ladislas_lucie-53aab2b3-7310-4ba6-b2bd-979d738cd81d.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-yann_hortense-a22c67a2-28c8-4cd4-a0e7-aec34e6c564f.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-yann_hortense-a22c67a2-28c8-4cd4-a0e7-aec34e6c564f.imageset/Contents.json new file mode 100644 index 0000000000..7d0288be75 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-yann_hortense-a22c67a2-28c8-4cd4-a0e7-aec34e6c564f.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-4_images_2_types-yann_hortense-a22c67a2-28c8-4cd4-a0e7-aec34e6c564f.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-yann_hortense-a22c67a2-28c8-4cd4-a0e7-aec34e6c564f.imageset/emotion_recognition-pictures-4_images_2_types-yann_hortense-a22c67a2-28c8-4cd4-a0e7-aec34e6c564f.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-yann_hortense-a22c67a2-28c8-4cd4-a0e7-aec34e6c564f.imageset/emotion_recognition-pictures-4_images_2_types-yann_hortense-a22c67a2-28c8-4cd4-a0e7-aec34e6c564f.pdf new file mode 100644 index 0000000000..984ab2c3e2 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_2_types-yann_hortense-a22c67a2-28c8-4cd4-a0e7-aec34e6c564f.imageset/emotion_recognition-pictures-4_images_2_types-yann_hortense-a22c67a2-28c8-4cd4-a0e7-aec34e6c564f.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_4_types-lucie_hortense_hugo_ladislas_yann-6dce5dd6-9bc8-4ae3-a6d1-24a531bda2bd.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_4_types-lucie_hortense_hugo_ladislas_yann-6dce5dd6-9bc8-4ae3-a6d1-24a531bda2bd.imageset/Contents.json new file mode 100644 index 0000000000..c7b48b70ce --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_4_types-lucie_hortense_hugo_ladislas_yann-6dce5dd6-9bc8-4ae3-a6d1-24a531bda2bd.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-pictures-4_images_4_types-lucie_hortense_hugo_ladislas_yann-6dce5dd6-9bc8-4ae3-a6d1-24a531bda2bd.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_4_types-lucie_hortense_hugo_ladislas_yann-6dce5dd6-9bc8-4ae3-a6d1-24a531bda2bd.imageset/emotion_recognition-pictures-4_images_4_types-lucie_hortense_hugo_ladislas_yann-6dce5dd6-9bc8-4ae3-a6d1-24a531bda2bd.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_4_types-lucie_hortense_hugo_ladislas_yann-6dce5dd6-9bc8-4ae3-a6d1-24a531bda2bd.imageset/emotion_recognition-pictures-4_images_4_types-lucie_hortense_hugo_ladislas_yann-6dce5dd6-9bc8-4ae3-a6d1-24a531bda2bd.pdf new file mode 100644 index 0000000000..ed97ca8d41 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-pictures-4_images_4_types-lucie_hortense_hugo_ladislas_yann-6dce5dd6-9bc8-4ae3-a6d1-24a531bda2bd.imageset/emotion_recognition-pictures-4_images_4_types-lucie_hortense_hugo_ladislas_yann-6dce5dd6-9bc8-4ae3-a6d1-24a531bda2bd.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_hortense-e631cc65-d3ed-4691-8adf-2290311d055a.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_hortense-e631cc65-d3ed-4691-8adf-2290311d055a.imageset/Contents.json new file mode 100644 index 0000000000..1960273bee --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_hortense-e631cc65-d3ed-4691-8adf-2290311d055a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-sounds-1_hortense-e631cc65-d3ed-4691-8adf-2290311d055a.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_hortense-e631cc65-d3ed-4691-8adf-2290311d055a.imageset/emotion_recognition-sounds-1_hortense-e631cc65-d3ed-4691-8adf-2290311d055a.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_hortense-e631cc65-d3ed-4691-8adf-2290311d055a.imageset/emotion_recognition-sounds-1_hortense-e631cc65-d3ed-4691-8adf-2290311d055a.pdf new file mode 100644 index 0000000000..8174da28d8 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_hortense-e631cc65-d3ed-4691-8adf-2290311d055a.imageset/emotion_recognition-sounds-1_hortense-e631cc65-d3ed-4691-8adf-2290311d055a.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_ladislas-d6144d9c-0bdd-4d65-86ed-a859c48c9de9.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_ladislas-d6144d9c-0bdd-4d65-86ed-a859c48c9de9.imageset/Contents.json new file mode 100644 index 0000000000..13de06fec2 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_ladislas-d6144d9c-0bdd-4d65-86ed-a859c48c9de9.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-sounds-1_ladislas-d6144d9c-0bdd-4d65-86ed-a859c48c9de9.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_ladislas-d6144d9c-0bdd-4d65-86ed-a859c48c9de9.imageset/emotion_recognition-sounds-1_ladislas-d6144d9c-0bdd-4d65-86ed-a859c48c9de9.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_ladislas-d6144d9c-0bdd-4d65-86ed-a859c48c9de9.imageset/emotion_recognition-sounds-1_ladislas-d6144d9c-0bdd-4d65-86ed-a859c48c9de9.pdf new file mode 100644 index 0000000000..7783cd98ee Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_ladislas-d6144d9c-0bdd-4d65-86ed-a859c48c9de9.imageset/emotion_recognition-sounds-1_ladislas-d6144d9c-0bdd-4d65-86ed-a859c48c9de9.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_lucie-125ff23b-2fc5-4e28-9eea-15833f014d1e.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_lucie-125ff23b-2fc5-4e28-9eea-15833f014d1e.imageset/Contents.json new file mode 100644 index 0000000000..30f61abd46 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_lucie-125ff23b-2fc5-4e28-9eea-15833f014d1e.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-sounds-1_lucie-125ff23b-2fc5-4e28-9eea-15833f014d1e.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_lucie-125ff23b-2fc5-4e28-9eea-15833f014d1e.imageset/emotion_recognition-sounds-1_lucie-125ff23b-2fc5-4e28-9eea-15833f014d1e.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_lucie-125ff23b-2fc5-4e28-9eea-15833f014d1e.imageset/emotion_recognition-sounds-1_lucie-125ff23b-2fc5-4e28-9eea-15833f014d1e.pdf new file mode 100644 index 0000000000..2fbe33405c Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-1_lucie-125ff23b-2fc5-4e28-9eea-15833f014d1e.imageset/emotion_recognition-sounds-1_lucie-125ff23b-2fc5-4e28-9eea-15833f014d1e.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-1c20ec40-acd6-423c-943c-b5bffc18d74a.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-1c20ec40-acd6-423c-943c-b5bffc18d74a.imageset/Contents.json new file mode 100644 index 0000000000..be68b56a01 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-1c20ec40-acd6-423c-943c-b5bffc18d74a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-sounds-2_hortense-1c20ec40-acd6-423c-943c-b5bffc18d74a.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-1c20ec40-acd6-423c-943c-b5bffc18d74a.imageset/emotion_recognition-sounds-2_hortense-1c20ec40-acd6-423c-943c-b5bffc18d74a.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-1c20ec40-acd6-423c-943c-b5bffc18d74a.imageset/emotion_recognition-sounds-2_hortense-1c20ec40-acd6-423c-943c-b5bffc18d74a.pdf new file mode 100644 index 0000000000..a6fd78bfde Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-1c20ec40-acd6-423c-943c-b5bffc18d74a.imageset/emotion_recognition-sounds-2_hortense-1c20ec40-acd6-423c-943c-b5bffc18d74a.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-ladislas-86c5d826-0f97-403c-a444-febb5060d220.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-ladislas-86c5d826-0f97-403c-a444-febb5060d220.imageset/Contents.json new file mode 100644 index 0000000000..a192d55a0a --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-ladislas-86c5d826-0f97-403c-a444-febb5060d220.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-sounds-2_hortense-ladislas-86c5d826-0f97-403c-a444-febb5060d220.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-ladislas-86c5d826-0f97-403c-a444-febb5060d220.imageset/emotion_recognition-sounds-2_hortense-ladislas-86c5d826-0f97-403c-a444-febb5060d220.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-ladislas-86c5d826-0f97-403c-a444-febb5060d220.imageset/emotion_recognition-sounds-2_hortense-ladislas-86c5d826-0f97-403c-a444-febb5060d220.pdf new file mode 100644 index 0000000000..e720a32d3d Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-ladislas-86c5d826-0f97-403c-a444-febb5060d220.imageset/emotion_recognition-sounds-2_hortense-ladislas-86c5d826-0f97-403c-a444-febb5060d220.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-lucie-058a9152-bbdb-482e-9f43-e9dbe898ca37.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-lucie-058a9152-bbdb-482e-9f43-e9dbe898ca37.imageset/Contents.json new file mode 100644 index 0000000000..dd0e206344 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-lucie-058a9152-bbdb-482e-9f43-e9dbe898ca37.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-sounds-2_hortense-lucie-058a9152-bbdb-482e-9f43-e9dbe898ca37.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-lucie-058a9152-bbdb-482e-9f43-e9dbe898ca37.imageset/emotion_recognition-sounds-2_hortense-lucie-058a9152-bbdb-482e-9f43-e9dbe898ca37.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-lucie-058a9152-bbdb-482e-9f43-e9dbe898ca37.imageset/emotion_recognition-sounds-2_hortense-lucie-058a9152-bbdb-482e-9f43-e9dbe898ca37.pdf new file mode 100644 index 0000000000..2b30737c52 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_hortense-lucie-058a9152-bbdb-482e-9f43-e9dbe898ca37.imageset/emotion_recognition-sounds-2_hortense-lucie-058a9152-bbdb-482e-9f43-e9dbe898ca37.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_ladislas-752f66df-8e15-4ef3-947e-d64d5d24f3a9.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_ladislas-752f66df-8e15-4ef3-947e-d64d5d24f3a9.imageset/Contents.json new file mode 100644 index 0000000000..3e5e514f5e --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_ladislas-752f66df-8e15-4ef3-947e-d64d5d24f3a9.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-sounds-2_ladislas-752f66df-8e15-4ef3-947e-d64d5d24f3a9.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_ladislas-752f66df-8e15-4ef3-947e-d64d5d24f3a9.imageset/emotion_recognition-sounds-2_ladislas-752f66df-8e15-4ef3-947e-d64d5d24f3a9.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_ladislas-752f66df-8e15-4ef3-947e-d64d5d24f3a9.imageset/emotion_recognition-sounds-2_ladislas-752f66df-8e15-4ef3-947e-d64d5d24f3a9.pdf new file mode 100644 index 0000000000..348019cd00 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_ladislas-752f66df-8e15-4ef3-947e-d64d5d24f3a9.imageset/emotion_recognition-sounds-2_ladislas-752f66df-8e15-4ef3-947e-d64d5d24f3a9.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_ladislas-lucie-791063d5-6c9a-4540-8495-2a49fa826acb.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_ladislas-lucie-791063d5-6c9a-4540-8495-2a49fa826acb.imageset/Contents.json new file mode 100644 index 0000000000..0d9b6f26db --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_ladislas-lucie-791063d5-6c9a-4540-8495-2a49fa826acb.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-sounds-2_ladislas-lucie-791063d5-6c9a-4540-8495-2a49fa826acb.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_ladislas-lucie-791063d5-6c9a-4540-8495-2a49fa826acb.imageset/emotion_recognition-sounds-2_ladislas-lucie-791063d5-6c9a-4540-8495-2a49fa826acb.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_ladislas-lucie-791063d5-6c9a-4540-8495-2a49fa826acb.imageset/emotion_recognition-sounds-2_ladislas-lucie-791063d5-6c9a-4540-8495-2a49fa826acb.pdf new file mode 100644 index 0000000000..6c843eb215 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_ladislas-lucie-791063d5-6c9a-4540-8495-2a49fa826acb.imageset/emotion_recognition-sounds-2_ladislas-lucie-791063d5-6c9a-4540-8495-2a49fa826acb.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_lucie-1a99dda4-c534-4577-a9be-c5683c43fc01.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_lucie-1a99dda4-c534-4577-a9be-c5683c43fc01.imageset/Contents.json new file mode 100644 index 0000000000..b5379ac238 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_lucie-1a99dda4-c534-4577-a9be-c5683c43fc01.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-sounds-2_lucie-1a99dda4-c534-4577-a9be-c5683c43fc01.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_lucie-1a99dda4-c534-4577-a9be-c5683c43fc01.imageset/emotion_recognition-sounds-2_lucie-1a99dda4-c534-4577-a9be-c5683c43fc01.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_lucie-1a99dda4-c534-4577-a9be-c5683c43fc01.imageset/emotion_recognition-sounds-2_lucie-1a99dda4-c534-4577-a9be-c5683c43fc01.pdf new file mode 100644 index 0000000000..6660dda56b Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-2_lucie-1a99dda4-c534-4577-a9be-c5683c43fc01.imageset/emotion_recognition-sounds-2_lucie-1a99dda4-c534-4577-a9be-c5683c43fc01.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-3_hortense-lucie-ladislas-85b0af03-cce2-4794-80b4-99f6f2b586c6.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-3_hortense-lucie-ladislas-85b0af03-cce2-4794-80b4-99f6f2b586c6.imageset/Contents.json new file mode 100644 index 0000000000..2f39d6d8d6 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-3_hortense-lucie-ladislas-85b0af03-cce2-4794-80b4-99f6f2b586c6.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-sounds-3_hortense-lucie-ladislas-85b0af03-cce2-4794-80b4-99f6f2b586c6.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-3_hortense-lucie-ladislas-85b0af03-cce2-4794-80b4-99f6f2b586c6.imageset/emotion_recognition-sounds-3_hortense-lucie-ladislas-85b0af03-cce2-4794-80b4-99f6f2b586c6.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-3_hortense-lucie-ladislas-85b0af03-cce2-4794-80b4-99f6f2b586c6.imageset/emotion_recognition-sounds-3_hortense-lucie-ladislas-85b0af03-cce2-4794-80b4-99f6f2b586c6.pdf new file mode 100644 index 0000000000..aa2bdfb658 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-3_hortense-lucie-ladislas-85b0af03-cce2-4794-80b4-99f6f2b586c6.imageset/emotion_recognition-sounds-3_hortense-lucie-ladislas-85b0af03-cce2-4794-80b4-99f6f2b586c6.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-3d16271d-8807-41ea-84f5-1631eeae5fba.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-3d16271d-8807-41ea-84f5-1631eeae5fba.imageset/Contents.json new file mode 100644 index 0000000000..ae32f94d69 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-3d16271d-8807-41ea-84f5-1631eeae5fba.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-sounds-4_hortense-3d16271d-8807-41ea-84f5-1631eeae5fba.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-3d16271d-8807-41ea-84f5-1631eeae5fba.imageset/emotion_recognition-sounds-4_hortense-3d16271d-8807-41ea-84f5-1631eeae5fba.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-3d16271d-8807-41ea-84f5-1631eeae5fba.imageset/emotion_recognition-sounds-4_hortense-3d16271d-8807-41ea-84f5-1631eeae5fba.pdf new file mode 100644 index 0000000000..4451fc223a Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-3d16271d-8807-41ea-84f5-1631eeae5fba.imageset/emotion_recognition-sounds-4_hortense-3d16271d-8807-41ea-84f5-1631eeae5fba.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-ladislas-8aeac9b2-6176-4f20-a8e6-6962d24a0e19.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-ladislas-8aeac9b2-6176-4f20-a8e6-6962d24a0e19.imageset/Contents.json new file mode 100644 index 0000000000..8492e3c529 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-ladislas-8aeac9b2-6176-4f20-a8e6-6962d24a0e19.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-sounds-4_hortense-ladislas-8aeac9b2-6176-4f20-a8e6-6962d24a0e19.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-ladislas-8aeac9b2-6176-4f20-a8e6-6962d24a0e19.imageset/emotion_recognition-sounds-4_hortense-ladislas-8aeac9b2-6176-4f20-a8e6-6962d24a0e19.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-ladislas-8aeac9b2-6176-4f20-a8e6-6962d24a0e19.imageset/emotion_recognition-sounds-4_hortense-ladislas-8aeac9b2-6176-4f20-a8e6-6962d24a0e19.pdf new file mode 100644 index 0000000000..6f72917b21 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-ladislas-8aeac9b2-6176-4f20-a8e6-6962d24a0e19.imageset/emotion_recognition-sounds-4_hortense-ladislas-8aeac9b2-6176-4f20-a8e6-6962d24a0e19.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-lucie-47264ade-a7b7-4ddf-b1d0-38e7eb34fac2.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-lucie-47264ade-a7b7-4ddf-b1d0-38e7eb34fac2.imageset/Contents.json new file mode 100644 index 0000000000..be92a4380b --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-lucie-47264ade-a7b7-4ddf-b1d0-38e7eb34fac2.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-sounds-4_hortense-lucie-47264ade-a7b7-4ddf-b1d0-38e7eb34fac2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-lucie-47264ade-a7b7-4ddf-b1d0-38e7eb34fac2.imageset/emotion_recognition-sounds-4_hortense-lucie-47264ade-a7b7-4ddf-b1d0-38e7eb34fac2.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-lucie-47264ade-a7b7-4ddf-b1d0-38e7eb34fac2.imageset/emotion_recognition-sounds-4_hortense-lucie-47264ade-a7b7-4ddf-b1d0-38e7eb34fac2.pdf new file mode 100644 index 0000000000..da93f58149 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_hortense-lucie-47264ade-a7b7-4ddf-b1d0-38e7eb34fac2.imageset/emotion_recognition-sounds-4_hortense-lucie-47264ade-a7b7-4ddf-b1d0-38e7eb34fac2.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_ladislas-466a969b-5ea2-4153-94f2-33737427b839.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_ladislas-466a969b-5ea2-4153-94f2-33737427b839.imageset/Contents.json new file mode 100644 index 0000000000..ccb0fae245 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_ladislas-466a969b-5ea2-4153-94f2-33737427b839.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-sounds-4_ladislas-466a969b-5ea2-4153-94f2-33737427b839.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_ladislas-466a969b-5ea2-4153-94f2-33737427b839.imageset/emotion_recognition-sounds-4_ladislas-466a969b-5ea2-4153-94f2-33737427b839.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_ladislas-466a969b-5ea2-4153-94f2-33737427b839.imageset/emotion_recognition-sounds-4_ladislas-466a969b-5ea2-4153-94f2-33737427b839.pdf new file mode 100644 index 0000000000..0c5225202c Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_ladislas-466a969b-5ea2-4153-94f2-33737427b839.imageset/emotion_recognition-sounds-4_ladislas-466a969b-5ea2-4153-94f2-33737427b839.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_ladislas-lucie-9b06de79-d1db-492b-a5f0-3ef0cf4aaada.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_ladislas-lucie-9b06de79-d1db-492b-a5f0-3ef0cf4aaada.imageset/Contents.json new file mode 100644 index 0000000000..89a269f71f --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_ladislas-lucie-9b06de79-d1db-492b-a5f0-3ef0cf4aaada.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-sounds-4_ladislas-lucie-9b06de79-d1db-492b-a5f0-3ef0cf4aaada.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_ladislas-lucie-9b06de79-d1db-492b-a5f0-3ef0cf4aaada.imageset/emotion_recognition-sounds-4_ladislas-lucie-9b06de79-d1db-492b-a5f0-3ef0cf4aaada.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_ladislas-lucie-9b06de79-d1db-492b-a5f0-3ef0cf4aaada.imageset/emotion_recognition-sounds-4_ladislas-lucie-9b06de79-d1db-492b-a5f0-3ef0cf4aaada.pdf new file mode 100644 index 0000000000..02c48e4b1f Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_ladislas-lucie-9b06de79-d1db-492b-a5f0-3ef0cf4aaada.imageset/emotion_recognition-sounds-4_ladislas-lucie-9b06de79-d1db-492b-a5f0-3ef0cf4aaada.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_lucie-0580093a-6e62-465d-bfe8-212d2bdcdb10.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_lucie-0580093a-6e62-465d-bfe8-212d2bdcdb10.imageset/Contents.json new file mode 100644 index 0000000000..9e954c4073 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_lucie-0580093a-6e62-465d-bfe8-212d2bdcdb10.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_recognition-sounds-4_lucie-0580093a-6e62-465d-bfe8-212d2bdcdb10.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_lucie-0580093a-6e62-465d-bfe8-212d2bdcdb10.imageset/emotion_recognition-sounds-4_lucie-0580093a-6e62-465d-bfe8-212d2bdcdb10.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_lucie-0580093a-6e62-465d-bfe8-212d2bdcdb10.imageset/emotion_recognition-sounds-4_lucie-0580093a-6e62-465d-bfe8-212d2bdcdb10.pdf new file mode 100644 index 0000000000..8f8fab3012 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/icons/emotion_recognition-sounds-4_lucie-0580093a-6e62-465d-bfe8-212d2bdcdb10.imageset/emotion_recognition-sounds-4_lucie-0580093a-6e62-465d-bfe8-212d2bdcdb10.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_hugo.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_hugo.imageset/Contents.json new file mode 100644 index 0000000000..9c74cb399f --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_hugo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_angry_hugo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_hugo.imageset/emotion_drawing_child_angry_hugo.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_hugo.imageset/emotion_drawing_child_angry_hugo.pdf new file mode 100644 index 0000000000..1ca27a692f Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_hugo.imageset/emotion_drawing_child_angry_hugo.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_louna.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_louna.imageset/Contents.json new file mode 100644 index 0000000000..41709de539 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_louna.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_angry_louna.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_louna.imageset/emotion_drawing_child_angry_louna.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_louna.imageset/emotion_drawing_child_angry_louna.pdf new file mode 100644 index 0000000000..3a8811290a Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_louna.imageset/emotion_drawing_child_angry_louna.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_moussa.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_moussa.imageset/Contents.json new file mode 100644 index 0000000000..a508aaae7c --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_moussa.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_angry_moussa.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_moussa.imageset/emotion_drawing_child_angry_moussa.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_moussa.imageset/emotion_drawing_child_angry_moussa.pdf new file mode 100644 index 0000000000..54ac4077ba Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_moussa.imageset/emotion_drawing_child_angry_moussa.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_oscar.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_oscar.imageset/Contents.json new file mode 100644 index 0000000000..409148fe7f --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_oscar.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_angry_oscar.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_oscar.imageset/emotion_drawing_child_angry_oscar.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_oscar.imageset/emotion_drawing_child_angry_oscar.pdf new file mode 100644 index 0000000000..36d1461ff7 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_oscar.imageset/emotion_drawing_child_angry_oscar.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_zoe.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_zoe.imageset/Contents.json new file mode 100644 index 0000000000..88bbda7fff --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_zoe.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_angry_zoe.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_zoe.imageset/emotion_drawing_child_angry_zoe.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_zoe.imageset/emotion_drawing_child_angry_zoe.pdf new file mode 100644 index 0000000000..c9dfca0386 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_angry_zoe.imageset/emotion_drawing_child_angry_zoe.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_hugo.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_hugo.imageset/Contents.json new file mode 100644 index 0000000000..7b100b10e6 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_hugo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_disgust_hugo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_hugo.imageset/emotion_drawing_child_disgust_hugo.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_hugo.imageset/emotion_drawing_child_disgust_hugo.pdf new file mode 100644 index 0000000000..66c6ba489f Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_hugo.imageset/emotion_drawing_child_disgust_hugo.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_louna.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_louna.imageset/Contents.json new file mode 100644 index 0000000000..54091ced34 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_louna.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_disgust_louna.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_louna.imageset/emotion_drawing_child_disgust_louna.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_louna.imageset/emotion_drawing_child_disgust_louna.pdf new file mode 100644 index 0000000000..f9a02bee5c Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_louna.imageset/emotion_drawing_child_disgust_louna.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_moussa.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_moussa.imageset/Contents.json new file mode 100644 index 0000000000..ee57ab86f2 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_moussa.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_disgust_moussa.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_moussa.imageset/emotion_drawing_child_disgust_moussa.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_moussa.imageset/emotion_drawing_child_disgust_moussa.pdf new file mode 100644 index 0000000000..160baa750a Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_moussa.imageset/emotion_drawing_child_disgust_moussa.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_oscar.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_oscar.imageset/Contents.json new file mode 100644 index 0000000000..990263f132 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_oscar.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_disgust_oscar.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_oscar.imageset/emotion_drawing_child_disgust_oscar.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_oscar.imageset/emotion_drawing_child_disgust_oscar.pdf new file mode 100644 index 0000000000..493fb763ba Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_oscar.imageset/emotion_drawing_child_disgust_oscar.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_zoe.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_zoe.imageset/Contents.json new file mode 100644 index 0000000000..0875b7157c --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_zoe.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_disgust_zoe.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_zoe.imageset/emotion_drawing_child_disgust_zoe.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_zoe.imageset/emotion_drawing_child_disgust_zoe.pdf new file mode 100644 index 0000000000..14f18ca7d1 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_disgust_zoe.imageset/emotion_drawing_child_disgust_zoe.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_hugo.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_hugo.imageset/Contents.json new file mode 100644 index 0000000000..7a4b2e31b2 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_hugo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_fear_hugo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_hugo.imageset/emotion_drawing_child_fear_hugo.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_hugo.imageset/emotion_drawing_child_fear_hugo.pdf new file mode 100644 index 0000000000..9f24e6d0eb Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_hugo.imageset/emotion_drawing_child_fear_hugo.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_louna.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_louna.imageset/Contents.json new file mode 100644 index 0000000000..2eac9fd30c --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_louna.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_fear_louna.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_louna.imageset/emotion_drawing_child_fear_louna.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_louna.imageset/emotion_drawing_child_fear_louna.pdf new file mode 100644 index 0000000000..8a99ac7f22 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_louna.imageset/emotion_drawing_child_fear_louna.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_moussa.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_moussa.imageset/Contents.json new file mode 100644 index 0000000000..94da124f26 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_moussa.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_fear_moussa.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_moussa.imageset/emotion_drawing_child_fear_moussa.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_moussa.imageset/emotion_drawing_child_fear_moussa.pdf new file mode 100644 index 0000000000..36c448f15b Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_moussa.imageset/emotion_drawing_child_fear_moussa.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_oscar.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_oscar.imageset/Contents.json new file mode 100644 index 0000000000..c767d4819d --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_oscar.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_fear_oscar.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_oscar.imageset/emotion_drawing_child_fear_oscar.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_oscar.imageset/emotion_drawing_child_fear_oscar.pdf new file mode 100644 index 0000000000..a2ad457982 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_oscar.imageset/emotion_drawing_child_fear_oscar.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_zoe.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_zoe.imageset/Contents.json new file mode 100644 index 0000000000..bf7b12faa0 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_zoe.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_fear_zoe.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_zoe.imageset/emotion_drawing_child_fear_zoe.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_zoe.imageset/emotion_drawing_child_fear_zoe.pdf new file mode 100644 index 0000000000..cf11daad51 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_fear_zoe.imageset/emotion_drawing_child_fear_zoe.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_hugo.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_hugo.imageset/Contents.json new file mode 100644 index 0000000000..fcef933b63 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_hugo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_joy_hugo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_hugo.imageset/emotion_drawing_child_joy_hugo.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_hugo.imageset/emotion_drawing_child_joy_hugo.pdf new file mode 100644 index 0000000000..e75a5437c3 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_hugo.imageset/emotion_drawing_child_joy_hugo.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_louna.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_louna.imageset/Contents.json new file mode 100644 index 0000000000..2a4b550e52 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_louna.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_joy_louna.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_louna.imageset/emotion_drawing_child_joy_louna.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_louna.imageset/emotion_drawing_child_joy_louna.pdf new file mode 100644 index 0000000000..8a982e0350 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_louna.imageset/emotion_drawing_child_joy_louna.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_moussa.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_moussa.imageset/Contents.json new file mode 100644 index 0000000000..6614e68fbd --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_moussa.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_joy_moussa.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_moussa.imageset/emotion_drawing_child_joy_moussa.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_moussa.imageset/emotion_drawing_child_joy_moussa.pdf new file mode 100644 index 0000000000..11453317e9 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_moussa.imageset/emotion_drawing_child_joy_moussa.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_oscar.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_oscar.imageset/Contents.json new file mode 100644 index 0000000000..6f4e897608 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_oscar.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_joy_oscar.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_oscar.imageset/emotion_drawing_child_joy_oscar.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_oscar.imageset/emotion_drawing_child_joy_oscar.pdf new file mode 100644 index 0000000000..93990a1594 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_oscar.imageset/emotion_drawing_child_joy_oscar.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_zoe.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_zoe.imageset/Contents.json new file mode 100644 index 0000000000..ffb760b374 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_zoe.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_joy_zoe.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_zoe.imageset/emotion_drawing_child_joy_zoe.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_zoe.imageset/emotion_drawing_child_joy_zoe.pdf new file mode 100644 index 0000000000..6a9b9d3ce8 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_joy_zoe.imageset/emotion_drawing_child_joy_zoe.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_hugo.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_hugo.imageset/Contents.json new file mode 100644 index 0000000000..062cf2d2f9 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_hugo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_sad_hugo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_hugo.imageset/emotion_drawing_child_sad_hugo.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_hugo.imageset/emotion_drawing_child_sad_hugo.pdf new file mode 100644 index 0000000000..c5dfac7436 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_hugo.imageset/emotion_drawing_child_sad_hugo.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_louna.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_louna.imageset/Contents.json new file mode 100644 index 0000000000..40173f655b --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_louna.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_sad_louna.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_louna.imageset/emotion_drawing_child_sad_louna.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_louna.imageset/emotion_drawing_child_sad_louna.pdf new file mode 100644 index 0000000000..7f03c479b6 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_louna.imageset/emotion_drawing_child_sad_louna.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_moussa.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_moussa.imageset/Contents.json new file mode 100644 index 0000000000..32de20a9c3 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_moussa.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_sad_moussa.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_moussa.imageset/emotion_drawing_child_sad_moussa.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_moussa.imageset/emotion_drawing_child_sad_moussa.pdf new file mode 100644 index 0000000000..8766c4830b Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_moussa.imageset/emotion_drawing_child_sad_moussa.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_oscar.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_oscar.imageset/Contents.json new file mode 100644 index 0000000000..3525dbbbc4 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_oscar.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_sad_oscar.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_oscar.imageset/emotion_drawing_child_sad_oscar.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_oscar.imageset/emotion_drawing_child_sad_oscar.pdf new file mode 100644 index 0000000000..ad782534e2 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_oscar.imageset/emotion_drawing_child_sad_oscar.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_zoe.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_zoe.imageset/Contents.json new file mode 100644 index 0000000000..272b892912 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_zoe.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_drawing_child_sad_zoe.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_zoe.imageset/emotion_drawing_child_sad_zoe.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_zoe.imageset/emotion_drawing_child_sad_zoe.pdf new file mode 100644 index 0000000000..7e66944b96 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_drawing_child_sad_zoe.imageset/emotion_drawing_child_sad_zoe.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_angry_leka.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_angry_leka.imageset/Contents.json new file mode 100644 index 0000000000..81cdbad779 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_angry_leka.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picto_angry_leka.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_angry_leka.imageset/emotion_picto_angry_leka.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_angry_leka.imageset/emotion_picto_angry_leka.pdf new file mode 100644 index 0000000000..b5eed528ac Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_angry_leka.imageset/emotion_picto_angry_leka.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_disgust_leka.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_disgust_leka.imageset/Contents.json new file mode 100644 index 0000000000..9ce8f3969e --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_disgust_leka.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picto_disgust_leka.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_disgust_leka.imageset/emotion_picto_disgust_leka.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_disgust_leka.imageset/emotion_picto_disgust_leka.pdf new file mode 100644 index 0000000000..607012c70f Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_disgust_leka.imageset/emotion_picto_disgust_leka.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_fear_leka.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_fear_leka.imageset/Contents.json new file mode 100644 index 0000000000..8c0cee4929 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_fear_leka.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picto_fear_leka.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_fear_leka.imageset/emotion_picto_fear_leka.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_fear_leka.imageset/emotion_picto_fear_leka.pdf new file mode 100644 index 0000000000..25910d53dd Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_fear_leka.imageset/emotion_picto_fear_leka.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_joy_leka.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_joy_leka.imageset/Contents.json new file mode 100644 index 0000000000..4186fa5692 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_joy_leka.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picto_joy_leka.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_joy_leka.imageset/emotion_picto_joy_leka.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_joy_leka.imageset/emotion_picto_joy_leka.pdf new file mode 100644 index 0000000000..a6afe7a421 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_joy_leka.imageset/emotion_picto_joy_leka.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_sad_leka.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_sad_leka.imageset/Contents.json new file mode 100644 index 0000000000..5f8922dd01 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_sad_leka.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picto_sad_leka.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_sad_leka.imageset/emotion_picto_sad_leka.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_sad_leka.imageset/emotion_picto_sad_leka.pdf new file mode 100644 index 0000000000..682af704af Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picto_sad_leka.imageset/emotion_picto_sad_leka.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_hortense.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_hortense.imageset/Contents.json new file mode 100644 index 0000000000..9bc0e90806 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_hortense.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_angry_hortense.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_hortense.imageset/emotion_picture_adult_angry_hortense.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_hortense.imageset/emotion_picture_adult_angry_hortense.pdf new file mode 100644 index 0000000000..0926cd9096 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_hortense.imageset/emotion_picture_adult_angry_hortense.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_hugo.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_hugo.imageset/Contents.json new file mode 100644 index 0000000000..5ac5b0ee05 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_hugo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_angry_hugo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_hugo.imageset/emotion_picture_adult_angry_hugo.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_hugo.imageset/emotion_picture_adult_angry_hugo.pdf new file mode 100644 index 0000000000..2245dc0554 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_hugo.imageset/emotion_picture_adult_angry_hugo.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_ladislas.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_ladislas.imageset/Contents.json new file mode 100644 index 0000000000..a05eb306d4 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_ladislas.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_angry_ladislas.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_ladislas.imageset/emotion_picture_adult_angry_ladislas.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_ladislas.imageset/emotion_picture_adult_angry_ladislas.pdf new file mode 100644 index 0000000000..66e6fe1639 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_ladislas.imageset/emotion_picture_adult_angry_ladislas.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_lucie.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_lucie.imageset/Contents.json new file mode 100644 index 0000000000..55de884ea3 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_lucie.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_angry_lucie.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_lucie.imageset/emotion_picture_adult_angry_lucie.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_lucie.imageset/emotion_picture_adult_angry_lucie.pdf new file mode 100644 index 0000000000..b8b6b7b13c Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_lucie.imageset/emotion_picture_adult_angry_lucie.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_yann.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_yann.imageset/Contents.json new file mode 100644 index 0000000000..dedd4e5237 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_yann.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_angry_yann.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_yann.imageset/emotion_picture_adult_angry_yann.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_yann.imageset/emotion_picture_adult_angry_yann.pdf new file mode 100644 index 0000000000..42b8266f89 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_angry_yann.imageset/emotion_picture_adult_angry_yann.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_hortense.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_hortense.imageset/Contents.json new file mode 100644 index 0000000000..ffc29c28f0 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_hortense.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_disgust_hortense.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_hortense.imageset/emotion_picture_adult_disgust_hortense.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_hortense.imageset/emotion_picture_adult_disgust_hortense.pdf new file mode 100644 index 0000000000..4867b7639e Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_hortense.imageset/emotion_picture_adult_disgust_hortense.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_hugo.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_hugo.imageset/Contents.json new file mode 100644 index 0000000000..5da332b773 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_hugo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_disgust_hugo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_hugo.imageset/emotion_picture_adult_disgust_hugo.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_hugo.imageset/emotion_picture_adult_disgust_hugo.pdf new file mode 100644 index 0000000000..efb77e5235 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_hugo.imageset/emotion_picture_adult_disgust_hugo.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_ladislas.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_ladislas.imageset/Contents.json new file mode 100644 index 0000000000..1ede92fe72 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_ladislas.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_disgust_ladislas.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_ladislas.imageset/emotion_picture_adult_disgust_ladislas.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_ladislas.imageset/emotion_picture_adult_disgust_ladislas.pdf new file mode 100644 index 0000000000..4f2217caea Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_ladislas.imageset/emotion_picture_adult_disgust_ladislas.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_lucie.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_lucie.imageset/Contents.json new file mode 100644 index 0000000000..358e030b94 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_lucie.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_disgust_lucie.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_lucie.imageset/emotion_picture_adult_disgust_lucie.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_lucie.imageset/emotion_picture_adult_disgust_lucie.pdf new file mode 100644 index 0000000000..ff3c911596 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_lucie.imageset/emotion_picture_adult_disgust_lucie.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_yann.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_yann.imageset/Contents.json new file mode 100644 index 0000000000..b383df14b7 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_yann.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_disgust_yann.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_yann.imageset/emotion_picture_adult_disgust_yann.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_yann.imageset/emotion_picture_adult_disgust_yann.pdf new file mode 100644 index 0000000000..b5316a3baa Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_disgust_yann.imageset/emotion_picture_adult_disgust_yann.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_hortense.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_hortense.imageset/Contents.json new file mode 100644 index 0000000000..ba93fc0d37 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_hortense.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_fear_hortense.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_hortense.imageset/emotion_picture_adult_fear_hortense.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_hortense.imageset/emotion_picture_adult_fear_hortense.pdf new file mode 100644 index 0000000000..43f4813bc3 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_hortense.imageset/emotion_picture_adult_fear_hortense.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_hugo.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_hugo.imageset/Contents.json new file mode 100644 index 0000000000..d737f43255 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_hugo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_fear_hugo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_hugo.imageset/emotion_picture_adult_fear_hugo.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_hugo.imageset/emotion_picture_adult_fear_hugo.pdf new file mode 100644 index 0000000000..f1d5795af0 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_hugo.imageset/emotion_picture_adult_fear_hugo.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_ladislas.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_ladislas.imageset/Contents.json new file mode 100644 index 0000000000..1a78da8ed4 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_ladislas.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_fear_ladislas.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_ladislas.imageset/emotion_picture_adult_fear_ladislas.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_ladislas.imageset/emotion_picture_adult_fear_ladislas.pdf new file mode 100644 index 0000000000..3b3e366964 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_ladislas.imageset/emotion_picture_adult_fear_ladislas.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_lucie.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_lucie.imageset/Contents.json new file mode 100644 index 0000000000..b7aa80ae56 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_lucie.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_fear_lucie.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_lucie.imageset/emotion_picture_adult_fear_lucie.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_lucie.imageset/emotion_picture_adult_fear_lucie.pdf new file mode 100644 index 0000000000..61e93268a7 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_lucie.imageset/emotion_picture_adult_fear_lucie.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_yann.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_yann.imageset/Contents.json new file mode 100644 index 0000000000..89ea7db869 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_yann.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_fear_yann.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_yann.imageset/emotion_picture_adult_fear_yann.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_yann.imageset/emotion_picture_adult_fear_yann.pdf new file mode 100644 index 0000000000..4bb7836c00 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_fear_yann.imageset/emotion_picture_adult_fear_yann.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_hortense.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_hortense.imageset/Contents.json new file mode 100644 index 0000000000..3ead179227 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_hortense.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_joy_hortense.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_hortense.imageset/emotion_picture_adult_joy_hortense.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_hortense.imageset/emotion_picture_adult_joy_hortense.pdf new file mode 100644 index 0000000000..5bc04002f8 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_hortense.imageset/emotion_picture_adult_joy_hortense.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_hugo.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_hugo.imageset/Contents.json new file mode 100644 index 0000000000..cbc7651788 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_hugo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_joy_hugo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_hugo.imageset/emotion_picture_adult_joy_hugo.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_hugo.imageset/emotion_picture_adult_joy_hugo.pdf new file mode 100644 index 0000000000..fc64d2b0cd Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_hugo.imageset/emotion_picture_adult_joy_hugo.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_ladislas.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_ladislas.imageset/Contents.json new file mode 100644 index 0000000000..7d3640c2a1 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_ladislas.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_joy_ladislas.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_ladislas.imageset/emotion_picture_adult_joy_ladislas.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_ladislas.imageset/emotion_picture_adult_joy_ladislas.pdf new file mode 100644 index 0000000000..834a653971 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_ladislas.imageset/emotion_picture_adult_joy_ladislas.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_lucie.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_lucie.imageset/Contents.json new file mode 100644 index 0000000000..8bf44e2471 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_lucie.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_joy_lucie.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_lucie.imageset/emotion_picture_adult_joy_lucie.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_lucie.imageset/emotion_picture_adult_joy_lucie.pdf new file mode 100644 index 0000000000..651bc2dbfd Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_lucie.imageset/emotion_picture_adult_joy_lucie.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_yann.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_yann.imageset/Contents.json new file mode 100644 index 0000000000..c8696b6086 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_yann.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_joy_yann.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_yann.imageset/emotion_picture_adult_joy_yann.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_yann.imageset/emotion_picture_adult_joy_yann.pdf new file mode 100644 index 0000000000..596fb9b507 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_joy_yann.imageset/emotion_picture_adult_joy_yann.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_hortense.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_hortense.imageset/Contents.json new file mode 100644 index 0000000000..65200583fb --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_hortense.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_sad_hortense.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_hortense.imageset/emotion_picture_adult_sad_hortense.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_hortense.imageset/emotion_picture_adult_sad_hortense.pdf new file mode 100644 index 0000000000..b33e7ab32a Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_hortense.imageset/emotion_picture_adult_sad_hortense.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_hugo.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_hugo.imageset/Contents.json new file mode 100644 index 0000000000..511897cc5c --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_hugo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_sad_hugo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_hugo.imageset/emotion_picture_adult_sad_hugo.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_hugo.imageset/emotion_picture_adult_sad_hugo.pdf new file mode 100644 index 0000000000..576f01cd5f Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_hugo.imageset/emotion_picture_adult_sad_hugo.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_ladislas.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_ladislas.imageset/Contents.json new file mode 100644 index 0000000000..2309594669 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_ladislas.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_sad_ladislas.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_ladislas.imageset/emotion_picture_adult_sad_ladislas.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_ladislas.imageset/emotion_picture_adult_sad_ladislas.pdf new file mode 100644 index 0000000000..7794744823 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_ladislas.imageset/emotion_picture_adult_sad_ladislas.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_lucie.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_lucie.imageset/Contents.json new file mode 100644 index 0000000000..6f572f25e3 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_lucie.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_sad_lucie.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_lucie.imageset/emotion_picture_adult_sad_lucie.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_lucie.imageset/emotion_picture_adult_sad_lucie.pdf new file mode 100644 index 0000000000..bf81e235bd Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_lucie.imageset/emotion_picture_adult_sad_lucie.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_yann.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_yann.imageset/Contents.json new file mode 100644 index 0000000000..d7d78590df --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_yann.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "emotion_picture_adult_sad_yann.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_yann.imageset/emotion_picture_adult_sad_yann.pdf b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_yann.imageset/emotion_picture_adult_sad_yann.pdf new file mode 100644 index 0000000000..41b73350ef Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/Activities-related Graphics/images/emotion_picture_adult_sad_yann.imageset/emotion_picture_adult_sad_yann.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 9221b9bb1a..353f8b25f6 100644 --- a/Apps/LekaApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Apps/LekaApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,93 +1,9 @@ { "images" : [ { - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "idiom" : "ios-marketing", - "scale" : "1x", + "filename" : "logo_icone_app.png", + "idiom" : "universal", + "platform" : "ios", "size" : "1024x1024" } ], diff --git a/Apps/LekaApp/Resources/Assets.xcassets/AppIcon.appiconset/logo_icone_app.png b/Apps/LekaApp/Resources/Assets.xcassets/AppIcon.appiconset/logo_icone_app.png new file mode 100644 index 0000000000..c9c4c345fc --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/AppIcon.appiconset/logo_icone_app.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff9087dc75721e32285eb09f7bc70aeb616f7ebe9b6c267652aaf885ffe036cb +size 103764 diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/big-joystick.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/big-joystick.imageset/Contents.json new file mode 100644 index 0000000000..ce0f3b6fec --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/big-joystick.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "big-joystick.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/big-joystick.imageset/big-joystick.svg b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/big-joystick.imageset/big-joystick.svg new file mode 100644 index 0000000000..121ec1c11e --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/big-joystick.imageset/big-joystick.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af3cd3beb88af739e8556a5dfa06135ed0a5354417eaca7b483791a0dd0ec176 +size 1579 diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/color-remote copy.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/color-remote copy.imageset/Contents.json new file mode 100644 index 0000000000..c179e03ada --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/color-remote copy.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "color-remote copy.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/color-remote copy.imageset/color-remote copy.svg b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/color-remote copy.imageset/color-remote copy.svg new file mode 100644 index 0000000000..032250a547 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/color-remote copy.imageset/color-remote copy.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de4af3651f2bde6c6c2c81aeb1729b7ffaddcab993c35571c76dd885b75113b6 +size 5209 diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/colored-arrows.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/colored-arrows.imageset/Contents.json new file mode 100644 index 0000000000..71265060d2 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/colored-arrows.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "colored-arrows.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/colored-arrows.imageset/colored-arrows.svg b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/colored-arrows.imageset/colored-arrows.svg new file mode 100644 index 0000000000..8a26c53028 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/colored-arrows.imageset/colored-arrows.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01eb69aee668ce7f706000deb2051ea25000d42dd7e678d1ba47314d9db1f096 +size 1424 diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/hand-remote.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/hand-remote.imageset/Contents.json new file mode 100644 index 0000000000..379f50add4 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/hand-remote.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "hand-remote.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/hand-remote.imageset/hand-remote.svg b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/hand-remote.imageset/hand-remote.svg new file mode 100644 index 0000000000..3d608914c1 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/hand-remote.imageset/hand-remote.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7f827510e728332b2726ff374ae724ffd0726592e2e349b6227987d3cf8c23a +size 2625 diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/standard-remote.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/standard-remote.imageset/Contents.json new file mode 100644 index 0000000000..2b06fc1b14 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/standard-remote.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "standard-remote.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/standard-remote.imageset/standard-remote.svg b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/standard-remote.imageset/standard-remote.svg new file mode 100644 index 0000000000..c7b586f410 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Commands/standard-remote.imageset/standard-remote.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b24ac38f5a26efd87dd79b12719144c84412f6a027d3a8f526957998831bd06b +size 3402 diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-1.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-1.imageset/Contents.json new file mode 100644 index 0000000000..18bc23ffbd --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-1.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "story-1.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-1.imageset/story-1.svg b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-1.imageset/story-1.svg new file mode 100644 index 0000000000..f4f4d31338 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-1.imageset/story-1.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce61abbd9877a1e9dcd4b4b6c03ff47d1f6d1c52c430be4da24eb9154ac77690 +size 4929 diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-2.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-2.imageset/Contents.json new file mode 100644 index 0000000000..df034075aa --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-2.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "story-2.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-2.imageset/story-2.svg b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-2.imageset/story-2.svg new file mode 100644 index 0000000000..984ade620c --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-2.imageset/story-2.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea9ce39f7c8b55eb8494e3dbd286c9ef973b65077c6f6b9b46284a298ed124b1 +size 6264 diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-3.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-3.imageset/Contents.json new file mode 100644 index 0000000000..821da37bc2 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-3.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "story-3.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-3.imageset/story-3.svg b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-3.imageset/story-3.svg new file mode 100644 index 0000000000..8d58c94ac2 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-3.imageset/story-3.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3e163ebb654930cd10d3dace1eea71d38ed65910a224b5440759d944adc7906 +size 6331 diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-4.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-4.imageset/Contents.json new file mode 100644 index 0000000000..ccad6e0772 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-4.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "story-4.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-4.imageset/story-4.svg b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-4.imageset/story-4.svg new file mode 100644 index 0000000000..9e453510a7 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-4.imageset/story-4.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbb5c4561ff8d9e7fd060bd8c1ebe2f271255ed7249533ad2cc1fd8d9450759a +size 6595 diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-5.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-5.imageset/Contents.json new file mode 100644 index 0000000000..2723c29bd5 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-5.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "story-5.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-5.imageset/story-5.svg b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-5.imageset/story-5.svg new file mode 100644 index 0000000000..52f7190dbf --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-5.imageset/story-5.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:500c8fc98a1e4ead9eeb3669b753914ae2ea246aa67492515593555104cbad0e +size 3926 diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-6.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-6.imageset/Contents.json new file mode 100644 index 0000000000..3c8640ce2a --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-6.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "story-6.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-6.imageset/story-6.svg b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-6.imageset/story-6.svg new file mode 100644 index 0000000000..d2bf51ed64 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/Icons Stories/story-6.imageset/story-6.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbe1153d1aea1a748958b1f48ff0f47276021cfe97ddbcb3557ebecd25cb2ed9 +size 4453 diff --git a/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Generalization.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Generalization.imageset/Contents.json new file mode 100644 index 0000000000..af250f6c25 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Generalization.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "parcours_Emotion_Recognition_Generalization.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Generalization.imageset/parcours_Emotion_Recognition_Generalization.pdf b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Generalization.imageset/parcours_Emotion_Recognition_Generalization.pdf new file mode 100644 index 0000000000..8d9cfb64c1 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Generalization.imageset/parcours_Emotion_Recognition_Generalization.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Images.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Images.imageset/Contents.json new file mode 100644 index 0000000000..bd8ae1cfee --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Images.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "parcours_Emotion_Recognition_Images.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Images.imageset/parcours_Emotion_Recognition_Images.pdf b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Images.imageset/parcours_Emotion_Recognition_Images.pdf new file mode 100644 index 0000000000..65fc49a8c2 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Images.imageset/parcours_Emotion_Recognition_Images.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Pictograms.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Pictograms.imageset/Contents.json new file mode 100644 index 0000000000..1cdacbc76a --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Pictograms.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "parcours_Emotion_Recognition_Pictograms.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Pictograms.imageset/parcours_Emotion_Recognition_Pictograms.pdf b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Pictograms.imageset/parcours_Emotion_Recognition_Pictograms.pdf new file mode 100644 index 0000000000..661b3e63ef Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Pictograms.imageset/parcours_Emotion_Recognition_Pictograms.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Pictures.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Pictures.imageset/Contents.json new file mode 100644 index 0000000000..e3081b39f2 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Pictures.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "parcours_Emotion_Recognition_Pictures.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Pictures.imageset/parcours_Emotion_Recognition_Pictures.pdf b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Pictures.imageset/parcours_Emotion_Recognition_Pictures.pdf new file mode 100644 index 0000000000..7dc9788241 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Pictures.imageset/parcours_Emotion_Recognition_Pictures.pdf differ diff --git a/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Sounds.imageset/Contents.json b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Sounds.imageset/Contents.json new file mode 100644 index 0000000000..e3081b39f2 --- /dev/null +++ b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Sounds.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "parcours_Emotion_Recognition_Pictures.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Sounds.imageset/parcours_Emotion_Recognition_Pictures.pdf b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Sounds.imageset/parcours_Emotion_Recognition_Pictures.pdf new file mode 100644 index 0000000000..ae97259515 Binary files /dev/null and b/Apps/LekaApp/Resources/Assets.xcassets/curriculums Icons/parcours_Emotion_Recognition_Sounds.imageset/parcours_Emotion_Recognition_Pictures.pdf differ diff --git a/Apps/LekaApp/Resources/GoogleService-Info.plist b/Apps/LekaApp/Resources/GoogleService-Info.plist new file mode 100644 index 0000000000..00d5af6a2a --- /dev/null +++ b/Apps/LekaApp/Resources/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyD_cqCvLrJYzfPOyZCOFAsBdfK-72MJE_A + GCM_SENDER_ID + 749287588285 + PLIST_VERSION + 1 + BUNDLE_ID + io.leka.apf.app.LekaApp + PROJECT_ID + leka-app-dev + STORAGE_BUCKET + leka-app-dev.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:749287588285:ios:0a6b2bbbbad6ff5eff92d4 + + diff --git a/Apps/LekaApp/Resources/Localizable.xcstrings b/Apps/LekaApp/Resources/Localizable.xcstrings new file mode 100644 index 0000000000..96c7f30ea0 --- /dev/null +++ b/Apps/LekaApp/Resources/Localizable.xcstrings @@ -0,0 +1,2238 @@ +{ + "version": "1.0", + "sourceLanguage": "en", + "strings": { + "edit_caregiver_view.appearance_section.accent_color_row.title": { + "comment": "AccentColor Row title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Color Theme" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Th\u00e8me de couleur" + } + } + } + }, + "edit_caregiver_view.appearance_section.appearance_row.title": { + "comment": "Appearance Row title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Dark Mode" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mode sombre" + } + } + } + }, + "edit_caregiver_view.close_button_label": { + "comment": "Close button label of Edit Caregiver View", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Close" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fermer" + } + } + } + }, + "edit_caregiver_view.navigation_title": { + "comment": "The navigation title of Edit Caregiver View", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Edit my profile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Editer mon profil" + } + } + } + }, + "edit_caregiver_view.save_button_label": { + "comment": "Save button label of Edit Caregiver View", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Save" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer" + } + } + } + }, + "edit_carereceiver_view.close_button_label": { + "comment": "Close button label of Edit Carereceiver View", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Close" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fermer" + } + } + } + }, + "edit_carereceiver_view.navigation_title": { + "comment": "The navigation title of Edit Carereceiver View", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Edit profil" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Editer le profil" + } + } + } + }, + "edit_carereceiver_view.save_button_label": { + "comment": "Save button label of Edit Carereceiver View", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Save" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer" + } + } + } + }, + "lekaapp.TextFieldEmail.invalidEmailErrorLabel": { + "comment": "TextFieldEmail invalid Email Error Label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "The email is not valid" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L'adresse email n'est pas valide" + } + } + } + }, + "lekaapp.TextFieldEmail.label": { + "comment": "TextFieldEmail label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Email" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Email" + } + } + } + }, + "lekaapp.TextFieldPassword.invalidPasswordErrorLabel": { + "comment": "TextFieldPassword invalid Password Error Label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "12 characters minimum, no spaces" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Minimum 12 caract\u00e8res, sans espaces" + } + } + } + }, + "lekaapp.TextFieldPassword.label": { + "comment": "TextFieldPassword label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Password" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mot de passe" + } + } + } + }, + "lekaapp.account_creation_process.step_1.go_button": { + "comment": "Step 1 continue button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Let's start! \ud83d\udc49" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Commen\u00e7ons ! \ud83d\udc49" + } + } + } + }, + "lekaapp.account_creation_process.step_1.message": { + "comment": "Step 1 message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "We will now guide you throught\nthe creation of your account\nand the different steps.\nReady?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nous allons maintenant vous guider\n\u00e0 travers la cr\u00e9ation de votre compte\net les diff\u00e9rentes \u00e9tapes.\nPr\u00eat(e)?" + } + } + } + }, + "lekaapp.account_creation_process.step_1.title": { + "comment": "Step 1 title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Welcome to our account creation process! \ud83d\udc4b" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bienvenue dans notre processus de cr\u00e9ation de compte ! \ud83d\udc4b" + } + } + } + }, + "lekaapp.account_creation_process.step_2.create_button": { + "comment": "Step 2 create button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Create" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cr\u00e9er" + } + } + } + }, + "lekaapp.account_creation_process.step_2.message": { + "comment": "Step 2 message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "First, let's create your profile as a caregiver." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout d'abord, cr\u00e9ons votre profil en tant qu'accompagnant." + } + } + } + }, + "lekaapp.account_creation_process.step_2.title": { + "comment": "Step 2 title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Step 1 - Caregiver profile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\u00c9tape 1 - Profil de l'accompagnant" + } + } + } + }, + "lekaapp.account_creation_process.step_3.create_button": { + "comment": "Step 3 create button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Create" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cr\u00e9er" + } + } + } + }, + "lekaapp.account_creation_process.step_3.message": { + "comment": "Step 3 message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "We will now create your first\ncare receiver profile." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nous allons maintenant cr\u00e9er un profil\nprofil de personne accompagn\u00e9e." + } + } + } + }, + "lekaapp.account_creation_process.step_3.title": { + "comment": "Step 3 title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Step 2 - Care receiver profile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\u00c9tape 2 - Profil de la personne accompagn\u00e9e" + } + } + } + }, + "lekaapp.account_creation_process.step_4.discover_content_button": { + "comment": "Step 4 discover content button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Let's go! \ud83d\ude80" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Allons-y ! \ud83d\ude80" + } + } + } + }, + "lekaapp.account_creation_process.step_4.message": { + "comment": "Step 4 message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "You have just completed:\n\n\u2705 Your caregiver profile\n\u2705 Your first care receiver profile\n\nYou can now discover the Leka App and dive deep in our educational content!" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous venez de cr\u00e9er :\n\n\u2705 Un profil d'accompagnant\n\u2705 Un profil de personne accompagn\u00e9e \n\nVous pouvez maintenant d\u00e9couvrir l'appli Leka et approfondir notre contenu \u00e9ducatif !" + } + } + } + }, + "lekaapp.account_creation_process.step_4.title": { + "comment": "Step 4 title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "\ud83c\udf89 Congratulations! \ud83d\udc4f" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\ud83c\udf89 F\u00e9licitations ! \ud83d\udc4f" + } + } + } + }, + "lekaapp.account_creation_view.connection_button": { + "comment": "Connection button on SignupView", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Connection" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connexion" + } + } + } + }, + "lekaapp.account_creation_view.create_account_title": { + "comment": "Create account title on SignupView", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Create an account" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cr\u00e9er un compte" + } + } + } + }, + "lekaapp.account_creation_view.email_verification_alert.dismissButton": { + "comment": "Email verification alert dismiss button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "OK" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "OK" + } + } + } + }, + "lekaapp.account_creation_view.email_verification_alert.message": { + "comment": "Email verification alert message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "An email has been sent to the address provided. Please click on the link to confirm your email address." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un email a \u00e9t\u00e9 envoy\u00e9 \u00e0 l'adresse indiqu\u00e9e. Veuillez cliquer sur le lien pour confirmer votre adresse e-mail." + } + } + } + }, + "lekaapp.account_creation_view.email_verification_alert.title": { + "comment": "Email verification alert title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Confirm your email address" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Confirmez votre adresse email" + } + } + } + }, + "lekaapp.account_creation_view.navigation_title": { + "comment": "NavigationBar title on the whole Signup process", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Account Creation Process" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Processus de cr\u00e9ation de compte" + } + } + } + }, + "lekaapp.activities_view.description": { + "comment": "Activities description", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + } + } + } + }, + "lekaapp.activities_view.subtitle": { + "comment": "Activities subtitle", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + } + } + } + }, + "lekaapp.activities_view.title": { + "comment": "Activities title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Activities" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activit\u00e9s" + } + } + } + }, + "lekaapp.avatar_picker.title": { + "comment": "Avatar picker title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Avatar choice" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choix de l'avatar" + } + } + } + }, + "lekaapp.avatar_picker.validate_button": { + "comment": "Avatar picker validate button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Validate selection" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Valider la s\u00e9lection" + } + } + } + }, + "lekaapp.caregiver_creation.avatar_choice_button": { + "comment": "Caregiver creation avatar choice button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Choose an avatar" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir un avatar" + } + } + } + }, + "lekaapp.caregiver_creation.caregiver_first_name_label": { + "comment": "Caregiver creation caregiver first name textfield label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "First name" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pr\u00e9nom" + } + } + } + }, + "lekaapp.caregiver_creation.caregiver_last_name_label": { + "comment": "Caregiver creation caregiver last name textfield label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Last name" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nom" + } + } + } + }, + "lekaapp.caregiver_creation.profession_add_button": { + "comment": "Caregiver creation profession add button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Add" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter" + } + } + } + }, + "lekaapp.caregiver_creation.profession_label": { + "comment": "Caregiver creation profession label above profession selection button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Profession(s)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Profession(s)" + } + } + } + }, + "lekaapp.caregiver_creation.register_profil_button": { + "comment": "Caregiver creation register profil button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Register profile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer ce profil" + } + } + } + }, + "lekaapp.caregiver_creation.title": { + "comment": "Caregiver creation title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Create a caregiver profile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cr\u00e9er un profil d'accompagnant" + } + } + } + }, + "lekaapp.caregiver_picker.add_button_label": { + "comment": "Caregiver picker add button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Add profile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter un profil" + } + } + } + }, + "lekaapp.caregiver_picker.add_first_caregiver.add_button_label": { + "comment": "Caregiver picker add first caregiver button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Add your first caregiver profile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cr\u00e9er votre premier profil Accompagnant" + } + } + } + }, + "lekaapp.caregiver_picker.add_first_caregiver.message": { + "comment": "Caregiver picker add first caregiver message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "No caregiver profiles have been created yet." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun profil d'accompagnant n'a \u00e9t\u00e9 cr\u00e9\u00e9 pour le moment." + } + } + } + }, + "lekaapp.caregiver_picker.close_button_label": { + "comment": "Caregiver picker close button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Close" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fermer" + } + } + } + }, + "lekaapp.caregiver_picker.title": { + "comment": "Caregiver picker title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Who are you ?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Qui \u00eates vous ? " + } + } + } + }, + "lekaapp.carereceiver_creation.avatar_choice_button": { + "comment": " Carereceiver creation avatar choice button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Choose an avatar" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir un avatar" + } + } + } + }, + "lekaapp.carereceiver_creation.carereceiver_name_label": { + "comment": " Carereceiver creation carereceiver name textfield label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Username" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nom d'utilisateur" + } + } + } + }, + "lekaapp.carereceiver_creation.register_profil_button": { + "comment": " Carereceiver creation register profil button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Register profile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer ce profil" + } + } + } + }, + "lekaapp.carereceiver_creation.title": { + "comment": " Carereceiver creation title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Create a carereceiver profile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cr\u00e9er un profil de personne accompagn\u00e9e" + } + } + } + }, + "lekaapp.carereceiver_list.add_button_label": { + "comment": "Carereceiver list add button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Add profile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter un profil" + } + } + } + }, + "lekaapp.carereceiver_list.add_first_carereceiver.add_button_label": { + "comment": "Carereceiver list add first carereceiver button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Add your first care receiver profile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cr\u00e9er votre premier profil de personne accompagn\u00e9e" + } + } + } + }, + "lekaapp.carereceiver_list.add_first_carereceiver.message": { + "comment": "Carereceiver list add first carereceiver message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "No care receiver profiles have been created yet." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun profil de personne accompagn\u00e9e n'a \u00e9t\u00e9 cr\u00e9\u00e9 pour le moment." + } + } + } + }, + "lekaapp.carereceiver_list.description": { + "comment": "Carereceiver list description", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + } + } + } + }, + "lekaapp.carereceiver_list.subtitle": { + "comment": "Carereceiver list subtitle", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + } + } + } + }, + "lekaapp.carereceiver_list.title": { + "comment": "Carereceiver list title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Care receivers" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Personnes accompagn\u00e9es" + } + } + } + }, + "lekaapp.carereceiver_picker.add_first_carereceiver.add_button_label": { + "comment": "Carereceiver picker add first carereceiver button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Go to care receiver section" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aller \u00e0 la section Personnes Accompagn\u00e9es" + } + } + } + }, + "lekaapp.carereceiver_picker.add_first_carereceiver.message": { + "comment": "Carereceiver picker add first carereceiver message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "No care receiver profiles have been created yet.\nYou can create one in the Care Receivers section." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun profil de personne accompagn\u00e9e n'a \u00e9t\u00e9 cr\u00e9\u00e9 pour le moment.\nVous pouvez en cr\u00e9er un en vous rendant dans la section Personnes Accompagn\u00e9es" + } + } + } + }, + "lekaapp.carereceiver_picker.close_button_label": { + "comment": "Carereceiver picker close button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Close" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fermer" + } + } + } + }, + "lekaapp.carereceiver_picker.skip_button_label": { + "comment": "Carereceiver picker continue without profile button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Continue without profile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Continuer sans profil" + } + } + } + }, + "lekaapp.carereceiver_picker.title": { + "comment": "Carereceiver picker title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Who do you do this activity with?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Avec qui faites vous cette activit\u00e9 ?" + } + } + } + }, + "lekaapp.carereceiver_picker.validate_button_label": { + "comment": "Carereceiver picker validate button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Validate" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Valider" + } + } + } + }, + "lekaapp.connection_view.connection_button": { + "comment": "ConnectionView connection button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Connect" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Se connecter" + } + } + } + }, + "lekaapp.connection_view.password_forgotten_button": { + "comment": "ConnectionView password forgotten button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "[Forgot password?](https://leka.io)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "[Mot de passe oubli\u00e9 ?](https://leka.io)" + } + } + } + }, + "lekaapp.connection_view.title": { + "comment": "ConnectionView title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Account connection" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connexion au compte" + } + } + } + }, + "lekaapp.curriculums_view.description": { + "comment": "Curriculums description", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + } + } + } + }, + "lekaapp.curriculums_view.subtitle": { + "comment": "Curriculums subtitle", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + } + } + } + }, + "lekaapp.curriculums_view.title": { + "comment": "Curriculums title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Curriculums" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Parcours" + } + } + } + }, + "lekaapp.news_view.description": { + "comment": "News description", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + } + } + } + }, + "lekaapp.news_view.subtitle": { + "comment": "News subtitle", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + } + } + } + }, + "lekaapp.news_view.title": { + "comment": "News title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "News" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveaut\u00e9s" + } + } + } + }, + "lekaapp.profession_picker.other_label": { + "comment": "Profession picker other profession label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Other (specify)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Autre (pr\u00e9ciser)" + } + } + } + }, + "lekaapp.profession_picker.title": { + "comment": "Profession picker title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Profession choice" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choix de(s) profession(s)" + } + } + } + }, + "lekaapp.profession_picker.validate_button": { + "comment": "Profession picker validate button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Validate selection" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Valider la s\u00e9lection" + } + } + } + }, + "lekaapp.reinforcer_picker.avatar_choice_button": { + "comment": " Reinforcer picker description", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Reinforcer is a repetitive light effect from the robot that you can activate to reward the user's behavior.\nIf your robot is connected, you can test the reinforcers before choosing one." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le renfor\u00e7ateur est un effet lumineux r\u00e9p\u00e9titif du robot que vous pourrez actionner pour r\u00e9compenser le comportement de l'utilisateur. Si votre robot est connect\u00e9, vous pouvez tester les renfor\u00e7ateurs avant d'en choisir un." + } + } + } + }, + "lekaapp.reinforcer_picker.header": { + "comment": "Reinforcer picker header", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Reinforcer choice" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choix du renfor\u00e7ateur" + } + } + } + }, + "lekaapp.remotes_view.description": { + "comment": "Remotes description", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + } + } + } + }, + "lekaapp.remotes_view.subtitle": { + "comment": "Remotes subtitle", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + } + } + } + }, + "lekaapp.remotes_view.title": { + "comment": "Remotes title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Remotes" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "T\u00e9l\u00e9commandes" + } + } + } + }, + "lekaapp.resources_view.description": { + "comment": "Resources description", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + } + } + } + }, + "lekaapp.resources_view.subtitle": { + "comment": "Resources subtitle", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + } + } + } + }, + "lekaapp.resources_view.title": { + "comment": "Resources title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Resources" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ressources" + } + } + } + }, + "lekaapp.robot_connection_label.text_connected_to": { + "comment": "Connected to xxx robot label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Connected to" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connect\u00e9 \u00e0" + } + } + } + }, + "lekaapp.robot_connection_label.text_not_connected": { + "comment": "Connect to your Leka label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Connect to your Leka" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connectez-vous \u00e0 votre Leka" + } + } + } + }, + "lekaapp.settings_label.title": { + "comment": "Settings button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Settings" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "R\u00e9glages" + } + } + } + }, + "lekaapp.welcome_view.create_account_button": { + "comment": "Create account button on WelcomeView", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Create account" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cr\u00e9er un compte" + } + } + } + }, + "lekaapp.welcome_view.login_button": { + "comment": "Login button on WelcomeView", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Login" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Se connecter" + } + } + } + }, + "lekaapp.welcome_view.skip_step_button": { + "comment": "Skip step button on WelcomeView", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Skip this step" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Passer cette \u00e9tape" + } + } + } + }, + "lekapp.carereceiver_view.available_soon_label": { + "comment": "Temporary content for carereceiver monitoring", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Your usage history will soon be available here." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre historique d'utilisation sera bient\u00f4t disponible ici." + } + } + } + }, + "lekapp.not_account_connected_label.button_label": { + "comment": "Button label to log in or sign up", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Log In or Sign Up" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Se Connecter ou Cr\u00e9er un Compte" + } + } + } + }, + "lekapp.not_account_connected_label.message": { + "comment": "Warning message when no account connected", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "You are currently using Leka without an account!." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous utilisez Leka sans \u00eatre connect\u00e9 !" + } + } + } + }, + "lekapp.sidebar.add_first_caregiver_profile.button_label": { + "comment": "The button label of create first profile when no profile is created", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Create First Caregiver" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cr\u00e9er un Profil Accompagnant" + } + } + } + }, + "lekapp.sidebar.carereceiver_view.edit_profile_button_label": { + "comment": "The button label of carereceiver profile editor", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Edit profile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\u00c9diter le profil" + } + } + } + }, + "lekapp.sidebar.edit_caregiver_profile.button_label": { + "comment": "The button label of caregiver profile editor", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Edit profile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\u00c9diter mon profil" + } + } + } + }, + "lekapp.sidebar.select_caregiver_profile.button_label": { + "comment": "The button label of select caregiver profile", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Select Caregiver" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "S\u00e9lectionner un Accompagnant" + } + } + } + }, + "main_view.sidebar.category_label.activities": { + "comment": "The title of the category 'Activities'", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Activities" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activit\u00e9s" + } + } + } + }, + "main_view.sidebar.category_label.carereceivers": { + "comment": "The title of the category 'Care Receivers'", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Care Receivers" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Personnes accompagn\u00e9es" + } + } + } + }, + "main_view.sidebar.category_label.curriculums": { + "comment": "The title of the category 'Curriculums'", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Curriculums" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Parcours" + } + } + } + }, + "main_view.sidebar.category_label.news": { + "comment": "The title of the category 'News'", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "News" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveaut\u00e9s" + } + } + } + }, + "main_view.sidebar.category_label.remotes": { + "comment": "The title of the category 'Remotes'", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Remotes" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "T\u00e9l\u00e9commandes" + } + } + } + }, + "main_view.sidebar.category_label.resources": { + "comment": "The title of the category 'Resources'", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Resources" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ressources" + } + } + } + }, + "main_view.sidebar.category_label.stories": { + "comment": "The title of the category 'Stories'", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Stories" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Histoires" + } + } + } + }, + "main_view.sidebar.navigation_title": { + "comment": "The title of the sidebar", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Leka App" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Leka App" + } + } + } + }, + "main_view.sidebar.section.content": { + "comment": "The title of the section 'Content'", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Content" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Contenu" + } + } + } + }, + "main_view.sidebar.section.information": { + "comment": "The title of the section 'Information'", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Information" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Informations" + } + } + } + }, + "main_view.sidebar.section.monitoring": { + "comment": "The title of the section 'Users'", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Users" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utilisateurs" + } + } + } + }, + "settings_view.account_section.delete_account.alert_message": { + "comment": "Delete account alert message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "To delete your account, please contact our support team at\nsupport@leka.io\nWe're here to help you with the process and answer any questions you might have." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pour supprimer votre compte, veuillez contacter notre \u00e9quipe support \u00e0\nsupport@leka.io\nNous sommes l\u00e0 pour vous aider dans le processus et r\u00e9pondre \u00e0 toutes vos questions." + } + } + } + }, + "settings_view.account_section.delete_account.alert_title": { + "comment": "Delete account alert title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Delete Account?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer Le Compte?" + } + } + } + }, + "settings_view.account_section.delete_account.button_label": { + "comment": "Delete account button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Delete Account" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer Le Compte" + } + } + } + }, + "settings_view.account_section.log_out.alert_buton_label": { + "comment": "Log out alert button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Log out" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Se d\u00e9connecter" + } + } + } + }, + "settings_view.account_section.log_out.alert_message": { + "comment": "Log out alert message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Are you sure you want to log out? You will need to enter your email and password to log back in." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\u00cates-vous s\u00fbr de vouloir vous d\u00e9connecter? Vous devrez saisir votre email et votre mot de passe pour vous reconnecter." + } + } + } + }, + "settings_view.account_section.log_out.alert_title": { + "comment": "Log out alert title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Confirm Logout" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Confirmer La D\u00e9connexion" + } + } + } + }, + "settings_view.account_section.log_out.button_label": { + "comment": "Log out button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Log Out" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Se D\u00e9connecter" + } + } + } + }, + "settings_view.account_section.log_out.error.alert_message": { + "comment": "Log out error alert message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "We encountered an issue logging you out. Please try again. If the problem persists, contact our support team for assistance." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nous avons rencontr\u00e9 un probl\u00e8me lors de votre d\u00e9connexion. Veuillez r\u00e9essayer. Si le probl\u00e8me persiste, contactez notre \u00e9quipe d'assistance pour obtenir de l'aide." + } + } + } + }, + "settings_view.account_section.log_out.error.alert_title": { + "comment": "Log out error alert title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Logout Error" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Erreur de d\u00e9connexion" + } + } + } + }, + "settings_view.change_language_section.button_label": { + "comment": "Change app language button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Change App Language" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Changer la Langue de l'Application" + } + } + } + }, + "settings_view.close_button_label": { + "comment": "Close button label of Settings View", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Close" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fermer" + } + } + } + }, + "settings_view.credentials_section.change_credentials.alert_message": { + "comment": "Change credentials alert message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "For security reasons, changes to your email or password need to be handled by our support team.\nPlease contact us at\nsupport@leka.io\nand we'll be happy to assist you with updating your account information." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pour des raisons de s\u00e9curit\u00e9, les modifications de l'e-mail ou du mot de passe doivent \u00eatre trait\u00e9es par notre \u00e9quipe support.\nVeuillez nous contacter \u00e0\nsupport@leka.io\net nous serons heureux de vous aider \u00e0 mettre \u00e0 jour les informations de votre compte." + } + } + } + }, + "settings_view.credentials_section.change_credentials.alert_title": { + "comment": "Change credentials alert title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Need to Update Your Credentials?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Besoin de mettre \u00e0 jour vos informations d'identification\u00a0?" + } + } + } + }, + "settings_view.credentials_section.change_credentials.button_label": { + "comment": "Change credentials button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Contact Support for Email/Password Change" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Contacter le support pour un changement d'e-mail/mot de passe" + } + } + } + }, + "settings_view.credentials_section.email_label": { + "comment": "Account email address label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Account Email" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Adresse Email" + } + } + } + }, + "settings_view.navigation_title": { + "comment": "The navigation title of Settings View", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Settings" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "R\u00e9glages" + } + } + } + }, + "settings_view.profiles_section.switch_profiles_button_label": { + "comment": "Switch caregiver profile button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Switch Caregiver Profile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Changer de Profil Accompagnant" + } + } + } + } + } +} diff --git a/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_angry_hortense.mp3 b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_angry_hortense.mp3 new file mode 100644 index 0000000000..140a9e60ee --- /dev/null +++ b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_angry_hortense.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e95b83680510c27277e4a32cb966f0b9bb3948bf5649b5799a3b386c7d59266a +size 96038 diff --git a/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_angry_ladislas.mp3 b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_angry_ladislas.mp3 new file mode 100644 index 0000000000..23e7cdb0db --- /dev/null +++ b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_angry_ladislas.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:773358b206ce806c725879759dd59d9b792e2b637de30ccb68a165ec7e664164 +size 59846 diff --git a/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_angry_lucie.mp3 b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_angry_lucie.mp3 new file mode 100644 index 0000000000..ed719663c6 --- /dev/null +++ b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_angry_lucie.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b3bf6f14e4f93d5e76cc1acbac53f3ff1dfef2d6df9f24631937dbfce0d7dea +size 55661 diff --git a/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_disgust_hortense.mp3 b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_disgust_hortense.mp3 new file mode 100644 index 0000000000..19c1b18ca2 --- /dev/null +++ b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_disgust_hortense.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5d3e9269de30087ea4cc03212bfaecf490ee5dc2b5f3acf41080e060155475e +size 37579 diff --git a/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_disgust_ladislas.mp3 b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_disgust_ladislas.mp3 new file mode 100644 index 0000000000..7cc1064d9a --- /dev/null +++ b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_disgust_ladislas.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d859283e495624607c57debc0681475a7ba2d9ed7406995838a958c9f12d9a64 +size 36325 diff --git a/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_disgust_lucie.mp3 b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_disgust_lucie.mp3 new file mode 100644 index 0000000000..5de1960b1c --- /dev/null +++ b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_disgust_lucie.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca9ac1429b53e7fe729f5bcca25fbd156cc5ca0ae251c14a094a24f4cf08e213 +size 37997 diff --git a/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_fear_hortense.mp3 b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_fear_hortense.mp3 new file mode 100644 index 0000000000..863b86f0d4 --- /dev/null +++ b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_fear_hortense.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:918cd850846aa855c9ef48389be48e1f4a651337f763a413b30704acca60b738 +size 29549 diff --git a/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_fear_ladislas.mp3 b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_fear_ladislas.mp3 new file mode 100644 index 0000000000..616bd9ac79 --- /dev/null +++ b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_fear_ladislas.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:411306ea586bbcc97c638652bc56ef01e0e7be23383b860388494ff8bc7fec6e +size 92486 diff --git a/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_fear_lucie.mp3 b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_fear_lucie.mp3 new file mode 100644 index 0000000000..60df7b20b4 --- /dev/null +++ b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_fear_lucie.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94287570d9d970e3681befebcf1aa6c6c36ec6df718e3f7d9fe33a5cee34166d +size 44390 diff --git a/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_joy_hortense.mp3 b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_joy_hortense.mp3 new file mode 100644 index 0000000000..b8f93f7a19 --- /dev/null +++ b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_joy_hortense.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e61966698b6fce2424ed7d953027066db6a2287a8c3ee721b1a24e5f4f4f2a3 +size 118406 diff --git a/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_joy_ladislas.mp3 b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_joy_ladislas.mp3 new file mode 100644 index 0000000000..b9fa61ae6a --- /dev/null +++ b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_joy_ladislas.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f62202d4db5875992de16851dbc89e93b86508006939521b6948d05771093a3 +size 80870 diff --git a/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_joy_lucie.mp3 b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_joy_lucie.mp3 new file mode 100644 index 0000000000..83b265c777 --- /dev/null +++ b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_joy_lucie.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa913425e01f6e4b1e1b216392bdbc206883ccdddde23bbfb8e6191beeaf79ee +size 78086 diff --git a/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_sad_hortense.mp3 b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_sad_hortense.mp3 new file mode 100644 index 0000000000..e21e599f5c --- /dev/null +++ b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_sad_hortense.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb1cdbdff010e9d7c71456a7242ddfe71cb06f146967f6172d37920fda0cdc39 +size 186278 diff --git a/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_sad_ladislas.mp3 b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_sad_ladislas.mp3 new file mode 100644 index 0000000000..f42bd0aecc --- /dev/null +++ b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_sad_ladislas.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c033b0874c6b029597da0503fd3236906a13135f19a55e327b7c71876c939ed1 +size 168854 diff --git a/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_sad_lucie.mp3 b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_sad_lucie.mp3 new file mode 100644 index 0000000000..b9eb5273a0 --- /dev/null +++ b/Apps/LekaApp/Resources/Media/Sounds/emotion_sound_adult_sad_lucie.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:128921e314c81713aef0d5af6c52d7d1db4aa8104d508fd0c84ce6c89750a523 +size 190310 diff --git a/Apps/LekaApp/Sources/LekaApp.swift b/Apps/LekaApp/Sources/LekaApp.swift deleted file mode 100644 index 755edd3ca2..0000000000 --- a/Apps/LekaApp/Sources/LekaApp.swift +++ /dev/null @@ -1,11 +0,0 @@ -import SwiftUI -import CoreUI - -@main -struct LekaApp: App { - var body: some Scene { - WindowGroup { - Hello("Leka App", in: .green) - } - } -} diff --git a/Apps/LekaApp/Sources/MainApp.swift b/Apps/LekaApp/Sources/MainApp.swift new file mode 100644 index 0000000000..023a7e0803 --- /dev/null +++ b/Apps/LekaApp/Sources/MainApp.swift @@ -0,0 +1,39 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import FirebaseCore +import LogKit +import SwiftUI + +let log = LogKit.createLoggerFor(app: "LekaApp") + +// MARK: - LekaApp + +@main +struct LekaApp: App { + // MARK: Lifecycle + + init() { + FirebaseApp.configure() + } + + // MARK: Internal + + @Environment(\.colorScheme) var colorScheme + + @ObservedObject var styleManager: StyleManager = .shared + + var body: some Scene { + WindowGroup { + MainView() + .onAppear { + self.styleManager.setDefaultColorScheme(self.colorScheme) + } + .tint(self.styleManager.accentColor) + .preferredColorScheme(self.styleManager.colorScheme) + } + } +} diff --git a/Apps/LekaApp/Sources/Preview Content/Preview Assets.xcassets/Contents.json b/Apps/LekaApp/Sources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Apps/LekaApp/Sources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Navigation/Navigation+Category.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Navigation/Navigation+Category.swift new file mode 100644 index 0000000000..40108ba1ce --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Navigation/Navigation+Category.swift @@ -0,0 +1,22 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +extension Navigation { + enum Category: Hashable, Identifiable, CaseIterable { + case news + case resources + case curriculums + case activities + case remotes + case sampleActivities + case carereceivers + case developerModeImageListPNG + + // MARK: Internal + + var id: Self { self } + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Navigation/Navigation.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Navigation/Navigation.swift new file mode 100644 index 0000000000..016cbabbbf --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Navigation/Navigation.swift @@ -0,0 +1,108 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import Combine +import ContentKit +import SwiftUI + +// MARK: - FullScreenCoverContent + +enum FullScreenCoverContent: Identifiable { + case welcomeView + case activityView + + // MARK: Internal + + var id: Self { self } +} + +// MARK: - SheetContent + +enum SheetContent: Hashable, Identifiable { + case robotConnection + case createCaregiver + case editCaregiver + case caregiverPicker + case carereceiverPicker(activity: Activity) + case settings + + // MARK: Internal + + var id: Self { self } +} + +// MARK: - Navigation + +class Navigation: ObservableObject { + // MARK: Lifecycle + + private init() { + self.subscribeAuthentificationStateUpdates() + } + + // MARK: Internal + + static let shared = Navigation() + + @Published var disableUICompletly: Bool = false + @Published var categories = Category.allCases + + @Published var sheetContent: SheetContent? + @Published var fullScreenCoverContent: FullScreenCoverContent? + + @Published var currentActivity: Activity? + + @Published var navigateToAccountCreationProcess: Bool = false + + var selectedCategory: Category? = .news { + willSet { + self.disableUICompletly = true + // ? Note: early return to avoid reseting path + guard !self.isProgrammaticNavigation else { return } + // TODO: (@ladislas) review this + // backupPath(for: selectedCategory) + } + didSet { + // TODO: (@ladislas) review this + // restorePath(for: selectedCategory) + } + } + + @Published var path: NavigationPath = .init() { + willSet { + self.disableUICompletly = true + } + didSet { + self.disableUICompletly = false + } + } + + // MARK: Private + + private var authManager: AuthManager = .shared + private var cancellables: Set = [] + + private var isProgrammaticNavigation: Bool = false + + private var pushPopNoAnimationTransaction: Transaction { + var transaction = Transaction(animation: nil) + transaction.disablesAnimations = true + return transaction + } + + private func subscribeAuthentificationStateUpdates() { + self.authManager.authenticationStatePublisher + .receive(on: DispatchQueue.main) + .sink { + if case $0 = AuthManager.AuthenticationState.loggedOut { + self.selectedCategory = .news + if self.sheetContent == nil, self.fullScreenCoverContent == nil { + self.fullScreenCoverContent = .welcomeView + } + } + } + .store(in: &self.cancellables) + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/AccountCreationView+l10n.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/AccountCreationView+l10n.swift new file mode 100644 index 0000000000..99cb19946e --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/AccountCreationView+l10n.swift @@ -0,0 +1,27 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +// swiftlint:disable line_length nesting + +extension l10n { + enum AccountCreationView { + enum EmailVerificationAlert { + static let title = LocalizedString("lekaapp.account_creation_view.email_verification_alert.title", value: "Confirm your email address", comment: "Email verification alert title") + + static let message = LocalizedString("lekaapp.account_creation_view.email_verification_alert.message", + value: "An email has been sent to the address provided. Please click on the link to confirm your email address.", + comment: "Email verification alert message") + + static let dismissButton = LocalizedString("lekaapp.account_creation_view.email_verification_alert.dismissButton", value: "OK", comment: "Email verification alert dismiss button") + } + + static let createAccountTitle = LocalizedString("lekaapp.account_creation_view.create_account_title", value: "Create an account", comment: "Create account title on SignupView") + + static let connectionButton = LocalizedString("lekaapp.account_creation_view.connection_button", value: "Connection", comment: "Connection button on SignupView") + } +} + +// swiftlint:enable line_length nesting diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/AccountCreationView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/AccountCreationView.swift new file mode 100644 index 0000000000..409c908993 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/AccountCreationView.swift @@ -0,0 +1,95 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - AccountCreationViewViewModel + +class AccountCreationViewViewModel: ObservableObject { + @Published var email: String = "" + @Published var password: String = "" +} + +// MARK: - AccountCreationView + +struct AccountCreationView: View { + // MARK: Internal + + var body: some View { + VStack(alignment: .center, spacing: 30) { + VStack(spacing: 10) { + Text(l10n.AccountCreationView.createAccountTitle) + .font(.title) + + Text(self.authManagerViewModel.errorMessage) + .font(.footnote) + .foregroundStyle(self.authManagerViewModel.showErrorAlert ? .red : .clear) + } + + VStack(spacing: 15) { + TextFieldEmail(entry: self.$viewModel.email) + TextFieldPassword(entry: self.$viewModel.password) + } + .frame(width: 400) + .disableAutocorrection(true) + + Button { + self.submitForm() + } label: { + Text(String(l10n.AccountCreationView.connectionButton.characters)) + .loadingText(isLoading: self.authManagerViewModel.isLoading) + } + .disabled(self.isCreationDisabled || self.authManagerViewModel.isLoading) + .buttonStyle(.borderedProminent) + } + .onChange(of: self.authManagerViewModel.userAuthenticationState) { newValue in + if newValue == .loggedIn { + self.rootAccountManager.createRootAccount(rootAccount: RootAccount()) + self.isVerificationEmailAlertPresented = true + } + } + .onAppear { + self.authManagerViewModel.userAction = .userIsSigningUp + } + .onDisappear { + self.authManagerViewModel.resetErrorMessage() + } + .alert(isPresented: self.$isVerificationEmailAlertPresented) { + Alert(title: Text(l10n.AccountCreationView.EmailVerificationAlert.title), + message: Text(l10n.AccountCreationView.EmailVerificationAlert.message), + dismissButton: .default(Text(l10n.AccountCreationView.EmailVerificationAlert.dismissButton)) { + self.navigation.navigateToAccountCreationProcess.toggle() + }) + } + } + + // MARK: Private + + @StateObject private var viewModel = AccountCreationViewViewModel() + + @ObservedObject private var authManagerViewModel = AuthManagerViewModel.shared + @ObservedObject private var navigation: Navigation = .shared + + @State private var isVerificationEmailAlertPresented: Bool = false + + private var authManager = AuthManager.shared + private var rootAccountManager = RootAccountManager.shared + + private var isCreationDisabled: Bool { + self.viewModel.email.isInvalidEmail() || self.viewModel.password.isInvalidPassword() + } + + private func submitForm() { + self.authManager.signUp(email: self.viewModel.email, password: self.viewModel.password) + } +} + +// MARK: - AccountCreationView_Previews + +#Preview { + AccountCreationView() +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess+Step1.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess+Step1.swift new file mode 100644 index 0000000000..bbfbb56f6f --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess+Step1.swift @@ -0,0 +1,38 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import SwiftUI + +extension AccountCreationProcess { + struct Step1: View { + @Binding var selectedTab: Step + + var body: some View { + VStack(spacing: 30) { + Text(l10n.AccountCreationProcess.Step1.title) + .font(.headline) + .foregroundColor(.orange) + + Text(l10n.AccountCreationProcess.Step1.message) + + Button(String(l10n.AccountCreationProcess.Step1.goButton.characters)) { + withAnimation { + self.selectedTab = .caregiverCreation + } + } + .buttonStyle(.bordered) + } + .padding() + .frame(width: 400) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .center) + } + } +} + +#Preview { + AccountCreationProcess.Step1(selectedTab: .constant(.intro)) +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess+Step2.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess+Step2.swift new file mode 100644 index 0000000000..7b5b1326ba --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess+Step2.swift @@ -0,0 +1,69 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import SwiftUI + +extension AccountCreationProcess { + struct Step2: View { + // MARK: Internal + + @Binding var selectedTab: Step + + var body: some View { + VStack(spacing: 30) { + Image(systemName: "person.3.fill") + .resizable() + .renderingMode(.original) + .aspectRatio(contentMode: .fit) + .frame(height: 80) + + Text(l10n.AccountCreationProcess.Step2.title) + .font(.headline) + .textCase(.uppercase) + .foregroundColor(.orange) + + Text(l10n.AccountCreationProcess.Step2.message) + + Button(String(l10n.AccountCreationProcess.Step2.createButton.characters)) { + withAnimation { + self.isCaregiverCreationPresented.toggle() + } + } + .buttonStyle(.bordered) + } + .padding() + .frame(width: 400) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .center) + .sheet(isPresented: self.$isCaregiverCreationPresented) { + NavigationStack { + CreateCaregiverView(onCreated: { caregiver in + self.caregiverManager.setCurrentCaregiver(to: caregiver) + }) + .navigationBarTitleDisplayMode(.inline) + } + } + .onReceive(self.caregiverManagerViewModel.$currentCaregiver) { + guard $0 != nil else { + return + } + self.selectedTab = .carereceiverCreation + } + } + + // MARK: Private + + @StateObject private var caregiverManagerViewModel = CaregiverManagerViewModel() + + @State private var isCaregiverCreationPresented: Bool = false + private let caregiverManager: CaregiverManager = .shared + } +} + +#Preview { + AccountCreationProcess.Step2(selectedTab: .constant(AccountCreationProcess.Step.caregiverCreation)) +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess+Step3.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess+Step3.swift new file mode 100644 index 0000000000..9f4de18de5 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess+Step3.swift @@ -0,0 +1,57 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import SwiftUI + +extension AccountCreationProcess { + struct Step3: View { + // MARK: Internal + + @Binding var selectedTab: Step + + var body: some View { + VStack(spacing: 30) { + Image(systemName: "figure.2.arms.open") + .resizable() + .renderingMode(.original) + .aspectRatio(contentMode: .fit) + .frame(height: 80) + + Text(l10n.AccountCreationProcess.Step3.title) + .font(.headline) + .textCase(.uppercase) + .foregroundColor(DesignKitAsset.Colors.lekaOrange.swiftUIColor) + + Text(l10n.AccountCreationProcess.Step3.message) + + Button(String(l10n.AccountCreationProcess.Step3.createButton.characters)) { + self.isCarereceiverCreationPresented.toggle() + } + .buttonStyle(.bordered) + } + .padding() + .frame(width: 400) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .center) + .sheet(isPresented: self.$isCarereceiverCreationPresented) { + NavigationStack { + CreateCarereceiverView(onCreated: { _ in + self.selectedTab = .final + }) + .navigationBarTitleDisplayMode(.inline) + } + } + } + + // MARK: Private + + @State private var isCarereceiverCreationPresented: Bool = false + } +} + +#Preview { + AccountCreationProcess.Step3(selectedTab: .constant(.carereceiverCreation)) +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess+Step4.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess+Step4.swift new file mode 100644 index 0000000000..31b6451eb7 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess+Step4.swift @@ -0,0 +1,43 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import SwiftUI + +extension AccountCreationProcess { + struct Step4: View { + // MARK: Internal + + var body: some View { + VStack(spacing: 30) { + Text(l10n.AccountCreationProcess.Step4.title) + .font(.headline) + .foregroundColor(DesignKitAsset.Colors.lekaOrange.swiftUIColor) + + Text(l10n.AccountCreationProcess.Step4.message) + + Button(String(l10n.AccountCreationProcess.Step4.discoverContentButton.characters)) { + self.authManagerViewModel.userAction = .none + self.navigation.fullScreenCoverContent = nil + } + .buttonStyle(.bordered) + } + .padding() + .frame(width: 400) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .center) + } + + // MARK: Private + + @ObservedObject private var authManagerViewModel = AuthManagerViewModel.shared + @ObservedObject private var navigation = Navigation.shared + } +} + +#Preview { + AccountCreationProcess.Step4() +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess+l10n.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess+l10n.swift new file mode 100644 index 0000000000..8db353b0d9 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess+l10n.swift @@ -0,0 +1,72 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +// swiftlint:disable line_length nesting + +extension l10n { + enum AccountCreationProcess { + enum Step1 { + static let title = LocalizedString("lekaapp.account_creation_process.step_1.title", + value: """ + Welcome to our account creation process! 👋 + """, + comment: "Step 1 title") + + static let message = LocalizedString("lekaapp.account_creation_process.step_1.message", + value: """ + We will now guide you throught + the creation of your account + and the different steps. + Ready? + """, + comment: "Step 1 message") + + static let goButton = LocalizedString("lekaapp.account_creation_process.step_1.go_button", value: "Let's start! 👉", comment: "Step 1 continue button") + } + + enum Step2 { + static let title = LocalizedString("lekaapp.account_creation_process.step_2.title", value: "Step 1 - Caregiver profile", comment: "Step 2 title") + + static let message = LocalizedString("lekaapp.account_creation_process.step_2.message", value: "First, let's create your profile as a caregiver.", comment: "Step 2 message") + + static let createButton = LocalizedString("lekaapp.account_creation_process.step_2.create_button", value: "Create", comment: "Step 2 create button") + } + + enum Step3 { + static let title = LocalizedString("lekaapp.account_creation_process.step_3.title", value: "Step 2 - Care receiver profile", comment: "Step 3 title") + + static let message = LocalizedString("lekaapp.account_creation_process.step_3.message", + value: """ + We will now create your first + care receiver profile. + """, + comment: "Step 3 message") + + static let createButton = LocalizedString("lekaapp.account_creation_process.step_3.create_button", value: "Create", comment: "Step 3 create button") + } + + enum Step4 { + static let title = LocalizedString("lekaapp.account_creation_process.step_4.title", value: "🎉 Congratulations! 👏", comment: "Step 4 title") + + static let message = LocalizedString("lekaapp.account_creation_process.step_4.message", + value: """ + You have just completed: + + ✅ Your caregiver profile + ✅ Your first care receiver profile + + You can now discover the Leka App and dive deep in our educational content! + """, + comment: "Step 4 message") + + static let discoverContentButton = LocalizedString("lekaapp.account_creation_process.step_4.discover_content_button", value: "Let's go! 🚀", comment: "Step 4 discover content button") + } + + static let navigationTitle = LocalizedString("lekaapp.account_creation_view.navigation_title", value: "Account Creation Process", comment: "NavigationBar title on the whole Signup process") + } +} + +// swiftlint:enable line_length nesting diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess.swift new file mode 100644 index 0000000000..8e29e66be9 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AccountCreation/Process/AccountCreationProcess.swift @@ -0,0 +1,50 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit +import SwiftUI + +enum AccountCreationProcess { + struct NavigationTitle: View { + var body: some View { + Text(l10n.AccountCreationProcess.navigationTitle) + .font(.headline) + } + } + + enum Step: Hashable { + case intro + case caregiverCreation + case carereceiverCreation + case final + } + + struct CarouselView: View { + // MARK: Internal + + var body: some View { + TabView(selection: self.$selectedTab) { + Step1(selectedTab: self.$selectedTab) + .tag(Step.intro) + Step2(selectedTab: self.$selectedTab) + .tag(Step.caregiverCreation) + Step3(selectedTab: self.$selectedTab) + .tag(Step.carereceiverCreation) + Step4() + .tag(Step.final) + } + .tabViewStyle(.page) + .indexViewStyle(.page(backgroundDisplayMode: .always)) + .toolbar { + ToolbarItem(placement: .principal) { + NavigationTitle() + } + } + } + + // MARK: Private + + @State private var selectedTab: Step = .intro + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Activities/SampleActivityListView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Activities/SampleActivityListView.swift new file mode 100644 index 0000000000..d12f9bc06a --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Activities/SampleActivityListView.swift @@ -0,0 +1,63 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import ContentKit +import GameEngineKit +import SwiftUI + +struct SampleActivityListView: View { + // MARK: Internal + + let activities: [Activity] = ContentKit.listSampleActivities() ?? [] + + var body: some View { + List { + ForEach(self.activities) { activity in + NavigationLink(destination: + ActivityDetailsView(activity: activity) + .toolbar { + ToolbarItem { + Button { + self.selectedActivity = activity + if self.authManagerViewModel.userAuthenticationState == .loggedIn { + self.navigation.sheetContent = .carereceiverPicker(activity: activity) + } else { + self.navigation.currentActivity = activity + self.navigation.fullScreenCoverContent = .activityView + } + } label: { + Image(systemName: "play.circle") + Text("Start activity") + } + .buttonStyle(.borderedProminent) + .tint(.lkGreen) + } + } + ) { + Image(uiImage: activity.details.iconImage) + .resizable() + .scaledToFit() + .frame(width: 44, height: 44) + .clipShape(Circle()) + + Text(activity.details.title) + } + } + } + .navigationTitle("Sample Activities") + } + + // MARK: Private + + @ObservedObject private var authManagerViewModel: AuthManagerViewModel = .shared + @ObservedObject private var navigation: Navigation = .shared + @State private var selectedActivity: Activity? +} + +#Preview { + NavigationStack { + SampleActivityListView() + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/AvatarPicker/AvatarPicker+AvatarCellLabel.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AvatarPicker/AvatarPicker+AvatarCellLabel.swift new file mode 100644 index 0000000000..0e3a1211ed --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AvatarPicker/AvatarPicker+AvatarCellLabel.swift @@ -0,0 +1,42 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import SwiftUI + +extension AvatarPicker { + struct AvatarCellLabel: View { + // MARK: Internal + + let image: UIImage + @Binding var isSelected: Bool + + var body: some View { + Image(uiImage: self.image) + .resizable() + .aspectRatio(contentMode: .fit) + .background(DesignKitAsset.Colors.blueGray.swiftUIColor) + .clipShape(Circle()) + .frame(maxWidth: 130, maxHeight: 130) + .animation(.default, value: self.isSelected) + .overlay( + Circle() + .stroke(self.styleManager.accentColor!, + lineWidth: self.isSelected ? 4 : 0) + ) + } + + // MARK: Private + + @ObservedObject private var styleManager: StyleManager = .shared + } +} + +#Preview { + HStack { + AvatarPicker.AvatarCellLabel(image: Avatars.iconToUIImage(icon: Avatars.categories[0].avatars[0]), isSelected: .constant(false)) + AvatarPicker.AvatarCellLabel(image: Avatars.iconToUIImage(icon: Avatars.categories[0].avatars[0]), isSelected: .constant(true)) + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/AvatarPicker/AvatarPicker+ButtonLabel.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AvatarPicker/AvatarPicker+ButtonLabel.swift new file mode 100644 index 0000000000..977ed43418 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AvatarPicker/AvatarPicker+ButtonLabel.swift @@ -0,0 +1,50 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import SwiftUI + +extension AvatarPicker { + struct ButtonLabel: View { + // MARK: Internal + + let image: String + + var body: some View { + Group { + if self.image == "" { + Image(systemName: "plus.circle.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 30) + .foregroundStyle(self.styleManager.accentColor!) + } else { + Image(uiImage: Avatars.iconToUIImage(icon: self.image)) + .resizable() + .aspectRatio(contentMode: .fit) + .background(DesignKitAsset.Colors.blueGray.swiftUIColor) + .clipShape(Circle()) + } + } + .frame(width: 125, height: 125) + .overlay( + Circle() + .stroke(self.styleManager.accentColor!, lineWidth: 2) + ) + } + + // MARK: Private + + @ObservedObject private var styleManager: StyleManager = .shared + } +} + +#Preview { + HStack { + AvatarPicker.ButtonLabel(image: "") + + AvatarPicker.ButtonLabel(image: Avatars.categories[0].avatars[0]) + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/AvatarPicker/AvatarPicker+ListView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AvatarPicker/AvatarPicker+ListView.swift new file mode 100644 index 0000000000..f307103aec --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AvatarPicker/AvatarPicker+ListView.swift @@ -0,0 +1,52 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import SwiftUI + +extension AvatarPicker { + struct ListView: View { + // MARK: Internal + + @Binding var selectedAvatar: String + + var body: some View { + List { + ForEach(Avatars.categories, id: \.self) { category in + if category.visible { + Section(category.name) { + ScrollView(.horizontal) { + LazyHGrid(rows: self.rows, spacing: 50) { + ForEach(category.avatars, id: \.self) { icon in + Button { + self.selectedAvatar = icon + } label: { + AvatarCellLabel( + image: Avatars.iconToUIImage(icon: icon), + isSelected: .constant(self.selectedAvatar == icon) + ) + } + .animation(.easeIn, value: self.selectedAvatar) + } + } + .padding() + } + } + .listRowBackground(Color.clear) + } + } + .listRowInsets(EdgeInsets()) + } + } + + // MARK: Private + + private let rows = [GridItem()] + } +} + +#Preview { + AvatarPicker.ListView(selectedAvatar: .constant("")) +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/AvatarPicker/AvatarPicker+l10n.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AvatarPicker/AvatarPicker+l10n.swift new file mode 100644 index 0000000000..574f307cfa --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AvatarPicker/AvatarPicker+l10n.swift @@ -0,0 +1,17 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +// swiftlint:disable line_length + +extension l10n { + enum AvatarPicker { + static let title = LocalizedString("lekaapp.avatar_picker.title", value: "Avatar choice", comment: "Avatar picker title") + + static let validateButton = LocalizedString("lekaapp.avatar_picker.validate_button", value: "Validate selection", comment: "Avatar picker validate button") + } +} + +// swiftlint:enable line_length diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/AvatarPicker/AvatarPicker.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AvatarPicker/AvatarPicker.swift new file mode 100644 index 0000000000..e7ef0bc7ed --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/AvatarPicker/AvatarPicker.swift @@ -0,0 +1,84 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import SwiftUI + +struct AvatarPicker: View { + // MARK: Lifecycle + + init(selectedAvatar: String, onCancel: (() -> Void)? = nil, onValidate: ((String) -> Void)? = nil) { + self.onCancel = onCancel + self.onValidate = onValidate + self._selectedAvatar = State(wrappedValue: selectedAvatar) + } + + // MARK: Internal + + @Environment(\.dismiss) var dismiss + @State var selectedAvatar: String = "" + let onCancel: (() -> Void)? + let onValidate: ((String) -> Void)? + + var body: some View { + ListView(selectedAvatar: self.$selectedAvatar) + .navigationTitle(String(l10n.AvatarPicker.title.characters)) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + self.action = .cancel + self.dismiss() + } label: { + Image(systemName: "xmark.circle") + } + } + ToolbarItem(placement: .navigationBarTrailing) { + Button { + self.action = .validate + self.dismiss() + } label: { + Label(String(l10n.AvatarPicker.validateButton.characters), systemImage: "checkmark.circle") + } + .disabled(self.selectedAvatar.isEmpty) + } + } + .onDisappear { + switch self.action { + case .cancel: + self.onCancel?() + case .validate: + self.onValidate?(self.selectedAvatar) + case .none: + break + } + + self.action = nil + } + } + + // MARK: Private + + private enum ActionType { + case cancel + case validate + } + + @State private var action: ActionType? +} + +#Preview { + NavigationStack { + AvatarPicker( + selectedAvatar: Avatars.categories[0].avatars[0], + onCancel: { + print("Avatar choice canceled") + }, + onValidate: { + print("You chose \($0)") + } + ) + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/ConnectionView+l10n.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/ConnectionView+l10n.swift new file mode 100644 index 0000000000..68bc3af62f --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/ConnectionView+l10n.swift @@ -0,0 +1,19 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +// swiftlint:disable nesting line_length + +extension l10n { + enum ConnectionView { + static let title = LocalizedString("lekaapp.connection_view.title", value: "Account connection", comment: "ConnectionView title") + + static let connectionButton = LocalizedString("lekaapp.connection_view.connection_button", value: "Connect", comment: "ConnectionView connection button") + + static let passwordForgottenButton = LocalizedString("lekaapp.connection_view.password_forgotten_button", value: "[Forgot password?](https://leka.io)", comment: "ConnectionView password forgotten button") + } +} + +// swiftlint:enable nesting line_length diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/ConnectionView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/ConnectionView.swift new file mode 100644 index 0000000000..406febc9a8 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/ConnectionView.swift @@ -0,0 +1,98 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - ConnectionViewViewModel + +// ? Make sure you have set up Associated Domains for your app and AutoFill Passwords +// ? is enabled in Settings in order to get the strong password proposals etc... +// ? the same applies for both login/signup +// ? re-enable autofill modifiers in TextFields when OK (textContentType) + +class ConnectionViewViewModel: ObservableObject { + @Published var email: String = "" + @Published var password: String = "" +} + +// MARK: - ConnectionView + +struct ConnectionView: View { + // MARK: Internal + + var body: some View { + VStack(alignment: .center, spacing: 30) { + VStack(spacing: 10) { + Text(l10n.ConnectionView.title) + .font(.title) + + Text(self.authManagerViewModel.errorMessage) + .font(.footnote) + .foregroundStyle(self.authManagerViewModel.showErrorAlert ? .red : .clear) + } + + VStack { + TextFieldEmail(entry: self.$viewModel.email) + + VStack { + TextFieldPassword(entry: self.$viewModel.password) + + Text(l10n.ConnectionView.passwordForgottenButton) + .font(.footnote) + .underline() + } + } + .disableAutocorrection(true) + .frame(width: 350) + + Button { + self.submitForm() + } label: { + Text(String(l10n.ConnectionView.connectionButton.characters)) + .loadingText(isLoading: self.authManagerViewModel.isLoading) + } + .disabled(self.isConnectionDisabled || self.authManagerViewModel.isLoading) + .buttonStyle(.borderedProminent) + } + .onChange(of: self.authManagerViewModel.userAuthenticationState) { newValue in + if newValue == .loggedIn { + self.caregiverManager.initializeCaregiversListener() + self.carereceiverManager.initializeCarereceiversListener() + self.authManagerViewModel.userAction = .none + self.navigation.fullScreenCoverContent = nil + } + } + .onAppear { + self.authManagerViewModel.userAction = .userIsSigningIn + } + .onDisappear { + self.authManagerViewModel.resetErrorMessage() + } + } + + // MARK: Private + + @StateObject private var viewModel = ConnectionViewViewModel() + @ObservedObject private var authManagerViewModel: AuthManagerViewModel = .shared + @ObservedObject private var navigation: Navigation = .shared + + private var authManager: AuthManager = .shared + private var caregiverManager: CaregiverManager = .shared + private var carereceiverManager: CarereceiverManager = .shared + + private var isConnectionDisabled: Bool { + self.viewModel.email.isEmpty || self.viewModel.password.isEmpty + } + + private func submitForm() { + self.authManager.signIn(email: self.viewModel.email, password: self.viewModel.password) + } +} + +#Preview { + ConnectionView() +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/ImageLists/DebugImageListView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/ImageLists/DebugImageListView.swift new file mode 100644 index 0000000000..7db80867f7 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/ImageLists/DebugImageListView.swift @@ -0,0 +1,113 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import GameEngineKit +import SwiftUI + +// MARK: - DebugImageListViewViewModel + +class DebugImageListViewViewModel: ObservableObject { + // MARK: Lifecycle + + init(images: [String]) { + self.images = images + } + + // MARK: Internal + + @Published var cellSize: CGFloat = 300 + @Published var cellState: GameplayChoiceState = .idle + @Published var images: [String] + + func getImageNameFromPath(path: String) -> String { + let components = path.components(separatedBy: "/") + return components.last ?? "" + } + + func printImagesNames() { + for (index, image) in self.images.enumerated() { + print("image \(index + 1)") + print("name: \(image)") + } + } + + func setState(to state: GameplayChoiceState) { + self.cellState = state + } + + func resizeWithAnimation(to size: CGFloat) { + withAnimation { + self.cellSize = size + } + } +} + +// MARK: - DebugImageListView + +struct DebugImageListView: View { + // MARK: Lifecycle + + init(images: [String]) { + self._viewModel = StateObject(wrappedValue: DebugImageListViewViewModel(images: images)) + } + + // MARK: Internal + + var body: some View { + ScrollView { + LazyVGrid(columns: self.columns, spacing: 20) { + ForEach(self.viewModel.images, id: \.self) { imageName in + VStack(spacing: 20) { + ChoiceImageView(image: imageName, size: self.viewModel.cellSize, state: self.viewModel.cellState) + + Text(self.viewModel.getImageNameFromPath(path: imageName)) + .lineLimit(2, reservesSpace: true) + .multilineTextAlignment(.center) + .font(.caption) + } + } + } + .frame(minWidth: 900) + .padding() + } + .background(.lkBackground) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + HStack(spacing: 0) { + Text("State:") + Button("idle") { self.viewModel.setState(to: .idle) } + Button("✅️") { self.viewModel.setState(to: .rightAnswer) } + Button("❌️") { self.viewModel.setState(to: .wrongAnswer) } + } + } + ToolbarItem(placement: .navigationBarTrailing) { + HStack(spacing: 0) { + Text("Size:") + Button("100") { self.viewModel.resizeWithAnimation(to: 100) } + Button("240") { self.viewModel.resizeWithAnimation(to: 240) } + Button("280") { self.viewModel.resizeWithAnimation(to: 280) } + Button("300") { self.viewModel.resizeWithAnimation(to: 300) } + } + } + } + .onAppear { + self.viewModel.printImagesNames() + } + } + + // MARK: Private + + @StateObject private var viewModel: DebugImageListViewViewModel + + private let columns = Array(repeating: GridItem(), count: 3) +} + +#Preview { + NavigationSplitView {} detail: { + NavigationStack { + DebugImageListView(images: ContentKit.listImagesPNG()) + } + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/ActivitiesView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/ActivitiesView.swift new file mode 100644 index 0000000000..fa0d5b4f21 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/ActivitiesView.swift @@ -0,0 +1,76 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - ActivitiesView + +struct ActivitiesView: View { + // MARK: Internal + + var body: some View { + VStack { + ScrollView(showsIndicators: true) { + HStack(alignment: .center, spacing: 30) { + Image(systemName: "dice") + .resizable() + .scaledToFit() + .frame(width: 80, height: 80) + .foregroundStyle(self.styleManager.accentColor!) + + VStack(alignment: .leading) { + Text(l10n.ActivitiesView.title) + .font(.largeTitle) + .fontWeight(.bold) + + Text(l10n.ActivitiesView.subtitle) + .font(.title2) + + Text(l10n.ActivitiesView.description) + .foregroundStyle(.secondary) + + Divider() + .padding(.top) + } + + Spacer() + } + .padding(.horizontal) + .padding() + } + } + } + + // MARK: Private + + @ObservedObject private var styleManager: StyleManager = .shared +} + +// MARK: - l10n.ActivitiesView + +// swiftlint:disable line_length + +extension l10n { + enum ActivitiesView { + static let title = LocalizedString("lekaapp.activities_view.title", + value: "Activities", + comment: "Activities title") + + static let subtitle = LocalizedString("lekaapp.activities_view.subtitle", + value: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + comment: "Activities subtitle") + + static let description = LocalizedString("lekaapp.activities_view.description", + value: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", + comment: "Activities description") + } +} + +// swiftlint:enable line_length + +#Preview { + ActivitiesView() +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/CarereceiverView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/CarereceiverView.swift new file mode 100644 index 0000000000..f9a8b58dfd --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/CarereceiverView.swift @@ -0,0 +1,109 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - CarereceiverView + +struct CarereceiverView: View { + // MARK: Internal + + @Environment(\.dismiss) var dismiss + @State var carereceiver: Carereceiver + + var body: some View { + VStack { + Button { + self.isEditCarereceiverViewPresented = true + } label: { + HStack(spacing: 20) { + Image(uiImage: Avatars.iconToUIImage(icon: self.carereceiver.avatar)) + .resizable() + .aspectRatio(contentMode: .fit) + .background(DesignKitAsset.Colors.blueGray.swiftUIColor) + .clipShape(Circle()) + .overlay { + Circle() + .strokeBorder(self.strokeColor, lineWidth: 2) + .background { + Circle() + .fill(Color(uiColor: UIColor.systemGray6)) + } + .overlay { + Image(uiImage: self.carereceiver.reinforcer.image) + .resizable() + .aspectRatio(contentMode: .fit) + .padding(5) + } + .frame(maxWidth: 45) + .offset(x: 45, y: 35) + } + VStack(alignment: .leading, spacing: 0) { + Text(self.carereceiver.username) + .font(.title) + .lineLimit(1) + .truncationMode(.tail) + .foregroundColor(.primary) + Text(l10n.CarereceiverView.editProfileButtonLabel) + .font(.footnote) + .foregroundStyle(self.styleManager.accentColor!) + } + } + } + .padding() + .frame(maxHeight: 140) + .sheet(isPresented: self.$isEditCarereceiverViewPresented) { + NavigationStack { + EditCarereceiverView(modifiedCarereceiver: self.$carereceiver) + .navigationBarTitleDisplayMode(.inline) + } + } + + Divider() + + Spacer() + + Image(systemName: "chart.xyaxis.line") + .resizable() + .scaledToFit() + .frame(maxWidth: 50) + Text(l10n.CarereceiverView.availableSoonLabel) + .font(.largeTitle) + .multilineTextAlignment(.center) + + Spacer() + } + } + + // MARK: Private + + private let strokeColor: Color = .init(light: UIColor.systemGray3, dark: UIColor.systemGray2) + @ObservedObject private var styleManager: StyleManager = .shared + @State private var isEditCarereceiverViewPresented = false +} + +// MARK: - l10n.CarereceiverView + +// swiftlint:disable line_length + +extension l10n { + enum CarereceiverView { + static let availableSoonLabel = LocalizedString("lekapp.carereceiver_view.available_soon_label", value: "Your usage history will soon be available here.", comment: "Temporary content for carereceiver monitoring") + + static let editProfileButtonLabel = LocalizedString("lekapp.sidebar.carereceiver_view.edit_profile_button_label", value: "Edit profile", comment: "The button label of carereceiver profile editor") + } +} + +// swiftlint:enable line_length + +#Preview { + CarereceiverView(carereceiver: Carereceiver( + username: "Peet", + avatar: Avatars.categories[0].avatars[0], + reinforcer: .fire + )) +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/CurriculumsView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/CurriculumsView.swift new file mode 100644 index 0000000000..1bf815bb65 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/CurriculumsView.swift @@ -0,0 +1,76 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - CurriculumsView + +struct CurriculumsView: View { + // MARK: Internal + + var body: some View { + VStack { + ScrollView(showsIndicators: true) { + HStack(alignment: .center, spacing: 30) { + Image(systemName: "graduationcap") + .resizable() + .scaledToFit() + .frame(width: 80, height: 80) + .foregroundStyle(self.styleManager.accentColor!) + + VStack(alignment: .leading) { + Text(l10n.CurriculumsView.title) + .font(.largeTitle) + .fontWeight(.bold) + + Text(l10n.CurriculumsView.subtitle) + .font(.title2) + + Text(l10n.CurriculumsView.description) + .foregroundStyle(.secondary) + + Divider() + .padding(.top) + } + + Spacer() + } + .padding(.horizontal) + .padding() + } + } + } + + // MARK: Private + + @ObservedObject private var styleManager: StyleManager = .shared +} + +// MARK: - l10n.CurriculumsView + +// swiftlint:disable line_length + +extension l10n { + enum CurriculumsView { + static let title = LocalizedString("lekaapp.curriculums_view.title", + value: "Curriculums", + comment: "Curriculums title") + + static let subtitle = LocalizedString("lekaapp.curriculums_view.subtitle", + value: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + comment: "Curriculums subtitle") + + static let description = LocalizedString("lekaapp.curriculums_view.description", + value: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", + comment: "Curriculums description") + } +} + +// swiftlint:enable line_length + +#Preview { + CurriculumsView() +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/EditCaregiverLabel.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/EditCaregiverLabel.swift new file mode 100644 index 0000000000..343f442340 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/EditCaregiverLabel.swift @@ -0,0 +1,153 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import RobotKit +import SwiftUI + +// MARK: - EditCaregiverLabel + +struct EditCaregiverLabel: View { + // MARK: Internal + + var body: some View { + VStack(alignment: .leading) { + if let caregiver = self.caregiverManagerViewModel.currentCaregiver { + Button { + self.navigation.sheetContent = .editCaregiver + } label: { + HStack(spacing: 10) { + Image(uiImage: Avatars.iconToUIImage(icon: caregiver.avatar)) + .resizable() + .aspectRatio(contentMode: .fit) + .background(DesignKitAsset.Colors.blueGray.swiftUIColor) + .clipShape(Circle()) + .frame(maxWidth: 90) + VStack(alignment: .leading, spacing: 0) { + Text(caregiver.firstName) + .font(.title) + .lineLimit(1) + .truncationMode(.tail) + .foregroundColor(.primary) + Text(caregiver.lastName) + .font(.caption) + .lineLimit(1) + .truncationMode(.tail) + .foregroundColor(.secondary) + Text(l10n.EditCaregiverProfile.buttonLabel) + .font(.footnote) + .foregroundStyle(self.styleManager.accentColor!) + } + } + } + } else if self.caregiverManagerViewModel.caregivers.isEmpty { + Button { + self.navigation.sheetContent = .createCaregiver + } label: { + VStack(spacing: 10) { + Image(systemName: "person.crop.circle.badge.plus") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 90) + .foregroundStyle(self.styleManager.accentColor!) + + Text(l10n.AddFirstCaregiverProfile.buttonLabel) + .foregroundStyle(self.styleManager.accentColor!) + .multilineTextAlignment(.center) + .padding(.horizontal) + } + } + } else { + Button { + self.navigation.sheetContent = .caregiverPicker + } label: { + VStack(spacing: 10) { + Image(systemName: "person.crop.circle.badge.questionmark") + .resizable() + .scaledToFit() + .frame(maxWidth: 90) + .foregroundStyle(self.styleManager.accentColor!) + + Text(l10n.SelectCaregiverProfile.buttonLabel) + .foregroundStyle(self.styleManager.accentColor!) + .multilineTextAlignment(.center) + .padding(.horizontal) + } + } + } + } + .frame(maxWidth: .infinity) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("", systemImage: "person.2.gobackward") { + self.navigation.sheetContent = .caregiverPicker + } + } + } + .onReceive(self.caregiverManagerViewModel.$currentCaregiver) { caregiverToEdit in + if self.navigation.sheetContent == nil, self.navigation.fullScreenCoverContent == nil { + if caregiverToEdit == nil { + self.navigation.sheetContent = .caregiverPicker + } + } + } + } + + // MARK: Private + + @ObservedObject private var styleManager: StyleManager = .shared + @ObservedObject private var navigation: Navigation = .shared + + @StateObject private var caregiverManagerViewModel = CaregiverManagerViewModel() +} + +// MARK: - l10n.ChangeCaregiverProfile + +// swiftlint:disable line_length + +extension l10n { + enum EditCaregiverProfile { + static let buttonLabel = LocalizedString("lekapp.sidebar.edit_caregiver_profile.button_label", value: "Edit profile", comment: "The button label of caregiver profile editor") + } + + enum SelectCaregiverProfile { + static let buttonLabel = LocalizedString("lekapp.sidebar.select_caregiver_profile.button_label", value: "Select Caregiver", comment: "The button label of select caregiver profile") + } + + enum AddFirstCaregiverProfile { + static let buttonLabel = LocalizedString("lekapp.sidebar.add_first_caregiver_profile.button_label", value: "Create First Caregiver", comment: "The button label of create first profile when no profile is created") + } +} + +// swiftlint:enable line_length + +#Preview { + NavigationSplitView(sidebar: { + List { + EditCaregiverLabel() + + Button {} label: { + RobotConnectionLabel() + } + .listRowInsets(EdgeInsets(top: 0, leading: -8, bottom: -8, trailing: -8)) + + Section("Information") { + Label("What's new?", systemImage: "lightbulb.max") + Label("Resources", systemImage: "book.and.wrench") + } + } + }, detail: { + EmptyView() + }) + .onAppear { + let caregiverManagerViewModel = CaregiverManagerViewModel() + caregiverManagerViewModel.currentCaregiver = Caregiver( + firstName: "Joe", + lastName: "Bidjobba", + avatar: Avatars.categories[0].avatars[2] + ) + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/MainView+CategoryLabel.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/MainView+CategoryLabel.swift new file mode 100644 index 0000000000..6218763b08 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/MainView+CategoryLabel.swift @@ -0,0 +1,65 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit +import SwiftUI + +extension MainView { + struct CategoryLabel: View { + // MARK: Lifecycle + + init(category: Navigation.Category) { + self.category = category + + switch category { + case .news: + self.title = String(l10n.MainView.Sidebar.CategoryLabel.news.characters) + self.systemImage = "lightbulb.max" + + case .resources: + self.title = String(l10n.MainView.Sidebar.CategoryLabel.resources.characters) + self.systemImage = "book.and.wrench" + + case .curriculums: + self.title = String(l10n.MainView.Sidebar.CategoryLabel.curriculums.characters) + self.systemImage = "graduationcap" + + case .activities: + self.title = String(l10n.MainView.Sidebar.CategoryLabel.activities.characters) + self.systemImage = "dice" + + case .remotes: + self.title = String(l10n.MainView.Sidebar.CategoryLabel.remotes.characters) + self.systemImage = "gamecontroller" + + case .sampleActivities: + self.title = "Sample activites" + self.systemImage = "testtube.2" + + case .carereceivers: + self.title = String(l10n.MainView.Sidebar.CategoryLabel.carereceivers.characters) + self.systemImage = "figure.2.arms.open" + + case .developerModeImageListPNG: + self.title = "PNG Image List" + self.systemImage = "photo.circle" + } + } + + // MARK: Internal + + let category: Navigation.Category + let title: String + let systemImage: String + + var body: some View { + Label(self.title, systemImage: self.systemImage) + .tag(self.category) + } + } +} + +#Preview { + MainView.CategoryLabel(category: .activities) +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/MainView+ViewModel.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/MainView+ViewModel.swift new file mode 100644 index 0000000000..c1bda0837e --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/MainView+ViewModel.swift @@ -0,0 +1,34 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import RobotKit +import SwiftUI + +extension MainView { + class ViewModel: ObservableObject { + // MARK: Lifecycle + + init() { + Robot.shared.isConnected + .receive(on: DispatchQueue.main) + .sink { [weak self] isConnected in + guard let self else { return } + self.isRobotConnect = isConnected + } + .store(in: &self.cancellables) + } + + // MARK: Internal + + @Published var isDesignSystemAppleExpanded: Bool = false + @Published var isDesignSystemLekaExpanded: Bool = false + + @Published var isRobotConnect: Bool = false + + // MARK: Private + + private var cancellables: Set = [] + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/MainView+l10n.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/MainView+l10n.swift new file mode 100644 index 0000000000..b764d67713 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/MainView+l10n.swift @@ -0,0 +1,33 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +// swiftlint:disable line_length nesting + +extension l10n { + enum MainView { + enum Sidebar { + enum CategoryLabel { + static let news = LocalizedString("main_view.sidebar.category_label.news", value: "News", comment: "The title of the category 'News'") + static let resources = LocalizedString("main_view.sidebar.category_label.resources", value: "Resources", comment: "The title of the category 'Resources'") + static let curriculums = LocalizedString("main_view.sidebar.category_label.curriculums", value: "Curriculums", comment: "The title of the category 'Curriculums'") + static let activities = LocalizedString("main_view.sidebar.category_label.activities", value: "Activities", comment: "The title of the category 'Activities'") + static let remotes = LocalizedString("main_view.sidebar.category_label.remotes", value: "Remotes", comment: "The title of the category 'Remotes'") + static let stories = LocalizedString("main_view.sidebar.category_label.stories", value: "Stories", comment: "The title of the category 'Stories'") + static let carereceivers = LocalizedString("main_view.sidebar.category_label.carereceivers", value: "Care Receivers", comment: "The title of the category 'Care Receivers'") + } + + static let navigationTitle = LocalizedString("main_view.sidebar.navigation_title", value: "Leka App", comment: "The title of the sidebar") + + static let sectionInformation = LocalizedString("main_view.sidebar.section.information", value: "Information", comment: "The title of the section 'Information'") + + static let sectionContent = LocalizedString("main_view.sidebar.section.content", value: "Content", comment: "The title of the section 'Content'") + + static let sectionUsers = LocalizedString("main_view.sidebar.section.monitoring", value: "Users", comment: "The title of the section 'Users'") + } + } +} + +// swiftlint:enable line_length nesting diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/MainView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/MainView.swift new file mode 100644 index 0000000000..5697fd4eaa --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/MainView.swift @@ -0,0 +1,184 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import Combine +import ContentKit +import DesignKit +import GameEngineKit +import LocalizationKit +import RobotKit +import SwiftUI + +extension Bundle { + static var version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + static var buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String +} + +// MARK: - MainView + +struct MainView: View { + // MARK: Internal + + @ObservedObject var navigation: Navigation = .shared + @ObservedObject var authManagerViewModel = AuthManagerViewModel.shared + @StateObject var viewModel: ViewModel = .init() + + var body: some View { + NavigationSplitView { + List(selection: self.$navigation.selectedCategory) { + if self.authManagerViewModel.userAuthenticationState == .loggedIn { + EditCaregiverLabel() + } else { + NoAccountConnectedLabel() + } + + Button { + self.navigation.sheetContent = .robotConnection + } label: { + RobotConnectionLabel() + } + .listRowInsets(EdgeInsets(top: 0, leading: -8, bottom: -8, trailing: -8)) + + Section(String(l10n.MainView.Sidebar.sectionInformation.characters)) { + CategoryLabel(category: .news) + CategoryLabel(category: .resources) + } + + Section(String(l10n.MainView.Sidebar.sectionContent.characters)) { + CategoryLabel(category: .curriculums) + CategoryLabel(category: .activities) + CategoryLabel(category: .remotes) + CategoryLabel(category: .sampleActivities) + } + + if self.authManagerViewModel.userAuthenticationState == .loggedIn { + Section(String(l10n.MainView.Sidebar.sectionUsers.characters)) { + CategoryLabel(category: .carereceivers) + } + } + + Section("Developer Mode") { + CategoryLabel(category: .developerModeImageListPNG) + } + + VStack(alignment: .center, spacing: 20) { + if self.authManagerViewModel.userAuthenticationState == .loggedIn { + Button { + self.navigation.sheetContent = .settings + } label: { + SettingsLabel() + } + } + + Text("My Leka App - Version \(Bundle.version!) (\(Bundle.buildNumber!))") + .foregroundColor(.gray) + .font(.caption2) + + LekaLogo(width: 50) + } + .frame(maxWidth: .infinity) + } + // TODO: (@ladislas) remove if not necessary + // .disabled(navigation.disableUICompletly) + } detail: { + NavigationStack(path: self.$navigation.path) { + switch self.navigation.selectedCategory { + case .news: + NewsView() + + case .resources: + ResourcesView() + + case .curriculums: + CurriculumsView() + + case .activities: + ActivitiesView() + + case .remotes: + RemotesView() + + case .sampleActivities: + SampleActivityListView() + + case .carereceivers: + CarereceiverList() + + case .developerModeImageListPNG: + DebugImageListView(images: ContentKit.listImagesPNG()) + .navigationTitle("PNG Image List") + + case .none: + Text("Select a category") + .font(.largeTitle) + .bold() + } + } + } + .id(self.authManagerViewModel.userAction) + .fullScreenCover(item: self.$navigation.fullScreenCoverContent) { + self.navigation.fullScreenCoverContent = nil + self.navigation.currentActivity = nil + } content: { content in + NavigationStack { + switch content { + case .welcomeView: + WelcomeView() + case .activityView: + ActivityView(activity: self.navigation.currentActivity!) + } + } + } + .sheet(item: self.$navigation.sheetContent) { + self.navigation.sheetContent = nil + } content: { content in + NavigationStack { + switch content { + case .robotConnection: + RobotConnectionView(viewModel: RobotConnectionViewModel()) + .navigationBarTitleDisplayMode(.inline) + case .settings: + SettingsView() + .navigationBarTitleDisplayMode(.inline) + case .editCaregiver: + EditCaregiverView(caregiver: self.caregiverManagerViewModel.currentCaregiver!) + .navigationBarTitleDisplayMode(.inline) + case .createCaregiver: + CreateCaregiverView(onCreated: { caregiver in + self.caregiverManager.setCurrentCaregiver(to: caregiver) + }) + .navigationBarTitleDisplayMode(.inline) + case .caregiverPicker: + CaregiverPicker() + .navigationBarTitleDisplayMode(.inline) + case let .carereceiverPicker(activity): + CarereceiverPicker(onDismiss: { + // nothing to do + }, onSelected: { carereceiver in + self.carereceiverManager.setCurrentCarereceiver(to: carereceiver) + self.navigation.currentActivity = activity + self.navigation.fullScreenCoverContent = .activityView + }, onSkip: { + self.navigation.currentActivity = activity + self.navigation.fullScreenCoverContent = .activityView + }) + .navigationBarTitleDisplayMode(.inline) + } + } + } + } + + // MARK: Private + + @StateObject private var caregiverManagerViewModel = CaregiverManagerViewModel() + + private var caregiverManager: CaregiverManager = .shared + private var carereceiverManager: CarereceiverManager = .shared +} + +#Preview { + MainView() + .previewInterfaceOrientation(.landscapeLeft) +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/NewsView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/NewsView.swift new file mode 100644 index 0000000000..61ea509b23 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/NewsView.swift @@ -0,0 +1,76 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - NewsView + +struct NewsView: View { + // MARK: Internal + + var body: some View { + VStack { + ScrollView(showsIndicators: true) { + HStack(alignment: .center, spacing: 30) { + Image(systemName: "lightbulb.max") + .resizable() + .scaledToFit() + .frame(width: 80, height: 80) + .foregroundStyle(self.styleManager.accentColor!) + + VStack(alignment: .leading) { + Text(l10n.NewsView.title) + .font(.largeTitle) + .fontWeight(.bold) + + Text(l10n.NewsView.subtitle) + .font(.title2) + + Text(l10n.NewsView.description) + .foregroundStyle(.secondary) + + Divider() + .padding(.top) + } + + Spacer() + } + .padding(.horizontal) + .padding() + } + } + } + + // MARK: Private + + @ObservedObject private var styleManager: StyleManager = .shared +} + +// MARK: - l10n.NewsView + +// swiftlint:disable line_length + +extension l10n { + enum NewsView { + static let title = LocalizedString("lekaapp.news_view.title", + value: "News", + comment: "News title") + + static let subtitle = LocalizedString("lekaapp.news_view.subtitle", + value: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + comment: "News subtitle") + + static let description = LocalizedString("lekaapp.news_view.description", + value: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", + comment: "News description") + } +} + +// swiftlint:enable line_length + +#Preview { + NewsView() +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/NoAccountConnectedLabel.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/NoAccountConnectedLabel.swift new file mode 100644 index 0000000000..7499ac2c9d --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/NoAccountConnectedLabel.swift @@ -0,0 +1,76 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import RobotKit +import SwiftUI + +// MARK: - NoAccountConnectedLabel + +struct NoAccountConnectedLabel: View { + // MARK: Internal + + var body: some View { + VStack(alignment: .center, spacing: 10) { + Image(systemName: "person.crop.circle.badge.xmark") + .resizable() + .renderingMode(.original) + .foregroundStyle(self.styleManager.accentColor!) + .scaledToFit() + .frame(width: 80) + + Text(l10n.NoAccountConnectedLabel.message) + .foregroundColor(.orange) + .font(.headline) + .multilineTextAlignment(.center) + + Button(String(l10n.NoAccountConnectedLabel.buttonLabel.characters)) { + self.navigation.fullScreenCoverContent = .welcomeView + } + .buttonStyle(.bordered) + } + .frame(maxWidth: .infinity) + } + + // MARK: Private + + @ObservedObject private var styleManager: StyleManager = .shared + @ObservedObject var navigation = Navigation.shared +} + +// MARK: - l10n.NoAccountConnectedLabel + +// swiftlint:disable line_length + +extension l10n { + enum NoAccountConnectedLabel { + static let message = LocalizedString("lekapp.not_account_connected_label.message", value: "You are currently using Leka without an account!.", comment: "Warning message when no account connected") + + static let buttonLabel = LocalizedString("lekapp.not_account_connected_label.button_label", value: "Log In or Sign Up", comment: "Button label to log in or sign up") + } +} + +// swiftlint:enable line_length + +#Preview { + NavigationSplitView(sidebar: { + List { + NoAccountConnectedLabel() + + Button {} label: { + RobotConnectionLabel() + } + .listRowInsets(EdgeInsets(top: 0, leading: -8, bottom: -8, trailing: -8)) + + Section("Information") { + Label("What's new?", systemImage: "lightbulb.max") + Label("Resources", systemImage: "book.and.wrench") + } + } + }, detail: { + EmptyView() + }) +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/RemotesView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/RemotesView.swift new file mode 100644 index 0000000000..d46945b717 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/RemotesView.swift @@ -0,0 +1,76 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - RemotesView + +struct RemotesView: View { + // MARK: Internal + + var body: some View { + VStack { + ScrollView(showsIndicators: true) { + HStack(alignment: .center, spacing: 30) { + Image(systemName: "gamecontroller") + .resizable() + .scaledToFit() + .frame(width: 80, height: 80) + .foregroundStyle(self.styleManager.accentColor!) + + VStack(alignment: .leading) { + Text(l10n.RemotesView.title) + .font(.largeTitle) + .fontWeight(.bold) + + Text(l10n.RemotesView.subtitle) + .font(.title2) + + Text(l10n.RemotesView.description) + .foregroundStyle(.secondary) + + Divider() + .padding(.top) + } + + Spacer() + } + .padding(.horizontal) + .padding() + } + } + } + + // MARK: Private + + @ObservedObject private var styleManager: StyleManager = .shared +} + +// MARK: - l10n.RemotesView + +// swiftlint:disable line_length + +extension l10n { + enum RemotesView { + static let title = LocalizedString("lekaapp.remotes_view.title", + value: "Remotes", + comment: "Remotes title") + + static let subtitle = LocalizedString("lekaapp.remotes_view.subtitle", + value: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + comment: "Remotes subtitle") + + static let description = LocalizedString("lekaapp.remotes_view.description", + value: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", + comment: "Remotes description") + } +} + +// swiftlint:enable line_length + +#Preview { + RemotesView() +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/ResourcesView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/ResourcesView.swift new file mode 100644 index 0000000000..f4f92063bd --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/ResourcesView.swift @@ -0,0 +1,76 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - ResourcesView + +struct ResourcesView: View { + // MARK: Internal + + var body: some View { + VStack { + ScrollView(showsIndicators: true) { + HStack(alignment: .center, spacing: 30) { + Image(systemName: "book.and.wrench") + .resizable() + .scaledToFit() + .frame(width: 80, height: 80) + .foregroundStyle(self.styleManager.accentColor!) + + VStack(alignment: .leading) { + Text(l10n.ResourcesView.title) + .font(.largeTitle) + .fontWeight(.bold) + + Text(l10n.ResourcesView.subtitle) + .font(.title2) + + Text(l10n.ResourcesView.description) + .foregroundStyle(.secondary) + + Divider() + .padding(.top) + } + + Spacer() + } + .padding(.horizontal) + .padding() + } + } + } + + // MARK: Private + + @ObservedObject private var styleManager: StyleManager = .shared +} + +// MARK: - l10n.ResourcesView + +// swiftlint:disable line_length + +extension l10n { + enum ResourcesView { + static let title = LocalizedString("lekaapp.resources_view.title", + value: "Resources", + comment: "Resources title") + + static let subtitle = LocalizedString("lekaapp.resources_view.subtitle", + value: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + comment: "Resources subtitle") + + static let description = LocalizedString("lekaapp.resources_view.description", + value: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", + comment: "Resources description") + } +} + +// swiftlint:enable line_length + +#Preview { + ResourcesView() +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/RobotConnectionLabel+IconIndicator.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/RobotConnectionLabel+IconIndicator.swift new file mode 100644 index 0000000000..4047b0f733 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/RobotConnectionLabel+IconIndicator.swift @@ -0,0 +1,81 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import RobotKit +import SwiftUI + +extension RobotConnectionLabel { + struct IconIndicator: View { + // MARK: Internal + + var isConnected: Bool + + var body: some View { + ZStack { + Circle() + .fill( + self.isConnected + ? DesignKitAsset.Colors.lekaGreen.swiftUIColor : DesignKitAsset.Colors.lekaDarkGray.swiftUIColor + ) + .opacity(0.4) + + Image( + uiImage: + self.isConnected + ? DesignKitAsset.Images.robotConnected.image : DesignKitAsset.Images.robotDisconnected.image + ) + .resizable() + .renderingMode(.original) + .aspectRatio(contentMode: .fit) + .frame(width: 44, height: 44, alignment: .center) + + Circle() + .stroke( + self.isConnected + ? DesignKitAsset.Colors.lekaGreen.swiftUIColor + : DesignKitAsset.Colors.lekaDarkGray.swiftUIColor, + lineWidth: 4 + ) + .frame(width: 44, height: 44) + } + .frame(width: 67, height: 67, alignment: .center) + .background( + Circle() + .fill(DesignKitAsset.Colors.lekaGreen.swiftUIColor) + .frame(width: self.diameter, height: self.diameter) + .opacity(self.isAnimated ? 0.0 : 0.8) + .animation( + Animation.easeInOut(duration: 1.5).delay(5).repeatForever(autoreverses: false), value: self.diameter + ) + .opacity(self.isConnected ? 1 : 0.0) + ) + .overlay( + alignment: .topTrailing, + content: { + if !self.isConnected { + Image(systemName: "exclamationmark.circle.fill") + .foregroundStyle(.white, .red) + } + } + ) + .onAppear { + self.isAnimated = true + self.diameter = self.isAnimated ? 100 : 0 + } + } + + // MARK: Private + + @State private var isAnimated: Bool = false + @State private var diameter: CGFloat = 0 + } +} + +#Preview { + HStack { + RobotConnectionLabel.IconIndicator(isConnected: true) + RobotConnectionLabel.IconIndicator(isConnected: false) + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/RobotConnectionLabel.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/RobotConnectionLabel.swift new file mode 100644 index 0000000000..ade6d359fb --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/RobotConnectionLabel.swift @@ -0,0 +1,109 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import RobotKit +import SwiftUI + +// MARK: - RobotConnectionLabel + +struct RobotConnectionLabel: View { + // MARK: Internal + + var body: some View { + HStack(spacing: 10) { + IconIndicator(isConnected: self.robotViewModel.isConnected) + self.buttonContent + Spacer() + } + .padding(.horizontal, 15) + .padding(.vertical, 10) + .background(self.backgroundColor, in: RoundedRectangle(cornerRadius: 10, style: .continuous)) + } + + // MARK: Private + + private let backgroundColor: Color = .init(light: UIColor.white, dark: UIColor.systemGray5) + + @StateObject private var robotViewModel: ConnectedRobotInformationViewModel = .init() + + @ViewBuilder + private var buttonContent: some View { + if self.robotViewModel.isConnected { + VStack(alignment: .leading, spacing: 4) { + Text(l10n.RobotConnectionLabel.textConnectedTo) + .font(.caption2) + + Text(self.robotViewModel.name) + .font(.subheadline) + + self.robotChargingStatusAndBattery + } + } else { + Text(l10n.RobotConnectionLabel.textNotConnected) + .font(.subheadline) + .multilineTextAlignment(.leading) + } + } + + private var robotChargingStatusAndBattery: some View { + HStack(spacing: 5) { + Text(verbatim: "LekaOS v\(self.robotViewModel.osVersion)") + .font(.footnote) + .foregroundColor(.gray) + + if self.robotViewModel.isCharging { + Image(systemName: "bolt.circle.fill") + .foregroundColor(.blue) + } else { + Image(systemName: "bolt.slash.circle") + .foregroundColor(.gray.opacity(0.6)) + } + + let battery = BatteryViewModel(level: robotViewModel.battery) + + Image(systemName: battery.name) + .foregroundColor(battery.color) + + Text(verbatim: "\(battery.level)%") + .font(.footnote) + .foregroundColor(.gray) + .monospacedDigit() + } + } +} + +// MARK: - l10n.RobotConnectionLabel + +extension l10n { + enum RobotConnectionLabel { + static let textConnectedTo = LocalizedString("lekaapp.robot_connection_label.text_connected_to", value: "Connected to", comment: "Connected to xxx robot label") + + static let textNotConnected = LocalizedString("lekaapp.robot_connection_label.text_not_connected", value: "Connect to your Leka", comment: "Connect to your Leka label") + } +} + +#Preview { + NavigationSplitView(sidebar: { + List { + HStack { + Spacer() + LekaLogo(width: 80) + Spacer() + } + Button {} label: { + RobotConnectionLabel() + } + .listRowInsets(EdgeInsets(top: 0, leading: -8, bottom: -8, trailing: -8)) + + Section("Information") { + Label("What's new?", systemImage: "lightbulb.max") + Label("Resources", systemImage: "book.and.wrench") + } + } + }, detail: { + EmptyView() + }) +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/SettingsLabel.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/SettingsLabel.swift new file mode 100644 index 0000000000..00107e713b --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/MainView/SettingsLabel.swift @@ -0,0 +1,37 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import RobotKit +import SwiftUI + +// MARK: - SettingsLabel + +struct SettingsLabel: View { + // MARK: Internal + + var body: some View { + Label(String(l10n.SettingsLabel.buttonLabel.characters), systemImage: "gear") + .frame(width: 200, height: 44) + .background(self.backgroundColor, in: RoundedRectangle(cornerRadius: 10, style: .continuous)) + .contentShape(Rectangle()) + } + + // MARK: Private + + private let backgroundColor: Color = .init(light: UIColor.white, dark: UIColor.systemGray5) +} + +// MARK: - l10n.SettingsLabel + +extension l10n { + enum SettingsLabel { + static let buttonLabel = LocalizedString("lekaapp.settings_label.title", value: "Settings", comment: "Settings button label") + } +} + +#Preview { + SettingsLabel() +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Modifiers/LoadingModifier.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Modifiers/LoadingModifier.swift new file mode 100644 index 0000000000..704d8a6baf --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Modifiers/LoadingModifier.swift @@ -0,0 +1,49 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// MARK: - LoadingModifier + +struct LoadingModifier: ViewModifier { + var isLoading: Bool + + func body(content: Content) -> some View { + content + .opacity(self.isLoading ? 0 : 1) + .overlay( + Group { + if self.isLoading { + ProgressView() + .tint(.white) + } + } + ) + .animation(.easeInOut, value: self.isLoading) + } +} + +extension View { + func loadingText(isLoading: Bool) -> some View { + modifier(LoadingModifier(isLoading: isLoading)) + } +} + +#Preview { + VStack { + Button {} + label: { + Text("Connect") + .loadingText(isLoading: false) + } + .buttonStyle(.borderedProminent) + + Button {} + label: { + Text("Connect") + .loadingText(isLoading: true) + } + .buttonStyle(.borderedProminent) + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/ProfessionPicker/ProfessionPicker+l10n.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/ProfessionPicker/ProfessionPicker+l10n.swift new file mode 100644 index 0000000000..6338c609e3 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/ProfessionPicker/ProfessionPicker+l10n.swift @@ -0,0 +1,19 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +// swiftlint:disable line_length + +extension l10n { + enum ProfessionPicker { + static let title = LocalizedString("lekaapp.profession_picker.title", value: "Profession choice", comment: "Profession picker title") + + static let otherLabel = LocalizedString("lekaapp.profession_picker.other_label", value: "Other (specify)", comment: "Profession picker other profession label") + + static let validateButton = LocalizedString("lekaapp.profession_picker.validate_button", value: "Validate selection", comment: "Profession picker validate button") + } +} + +// swiftlint:enable line_length diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/ProfessionPicker/ProfessionPicker.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/ProfessionPicker/ProfessionPicker.swift new file mode 100644 index 0000000000..be293dd41a --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/ProfessionPicker/ProfessionPicker.swift @@ -0,0 +1,112 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - ProfessionPicker + +struct ProfessionPicker: View { + // MARK: Lifecycle + + init(selectedProfessionsIDs: [String], onCancel: (() -> Void)? = nil, onValidate: (([String]) -> Void)? = nil) { + self.onCancel = onCancel + self.onValidate = onValidate + self.selectedProfessionsIDs = selectedProfessionsIDs + } + + // MARK: Internal + + @Environment(\.dismiss) var dismiss + @State var selectedProfessions: Set = [] + let selectedProfessionsIDs: [String] + let onCancel: (() -> Void)? + let onValidate: (([String]) -> Void)? + + var body: some View { + List(Professions.list, id: \.self, selection: self.$selectedProfessions) { profession in + HStack { + Text(profession.name) + Spacer() + Image(systemName: "info.circle") + .onTapGesture { + self.selectedProfessionForDetails = profession + } + .foregroundStyle(self.selectedProfessions.contains(profession) ? Color.white : Color.accentColor) + } + } + .environment(\.editMode, Binding.constant(EditMode.active)) + .navigationTitle(String(l10n.ProfessionPicker.title.characters)) + .onAppear { + let professions = self.selectedProfessionsIDs.compactMap { Professions.profession(for: $0) } + self.selectedProfessions = Set(professions) + } + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + self.action = .cancel + self.dismiss() + } label: { + Image(systemName: "xmark.circle") + } + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button { + self.action = .validate + self.dismiss() + } label: { + Label(String(l10n.ProfessionPicker.validateButton.characters), systemImage: "checkmark.circle") + } + .disabled(self.selectedProfessions.isEmpty) + } + } + .sheet(item: self.$selectedProfessionForDetails, onDismiss: { self.selectedProfessionForDetails = nil }, content: { profession in + VStack(spacing: 40) { + Text(profession.name) + .font(.largeTitle) + Text(profession.description) + .padding(.horizontal, 20) + .font(.title2) + } + }) + .onDisappear { + switch self.action { + case .cancel: + self.onCancel?() + case .validate: + // swiftformat:disable:next preferKeyPath + let professionIDs = self.selectedProfessions.compactMap { $0.id } + self.onValidate?(Array(professionIDs)) + case .none: + break + } + + self.action = nil + } + } + + // MARK: Private + + private enum ActionType { + case cancel + case validate + } + + @State private var action: ActionType? + @State private var selectedProfessionForDetails: Profession? +} + +// MARK: - ProfessionPicker_Previews + +#Preview { + NavigationStack { + ProfessionPicker(selectedProfessionsIDs: Caregiver().professions, + onValidate: { + print("Selected professions: \($0)") + }) + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/ReinforcerPicker/ReinforcerPicker.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/ReinforcerPicker/ReinforcerPicker.swift new file mode 100644 index 0000000000..ab1f492ee9 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/ReinforcerPicker/ReinforcerPicker.swift @@ -0,0 +1,66 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import RobotKit +import SwiftUI + +// MARK: - ReinforcerPicker + +struct ReinforcerPicker: View { + // MARK: Internal + + @Binding var carereceiver: Carereceiver + + var body: some View { + HStack { + ForEach(Robot.Reinforcer.allCases, id: \.self) { reinforcer in + Image(uiImage: reinforcer.image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 50) + .padding(5) + .background( + Circle() + .stroke(self.styleManager.accentColor!, lineWidth: self.carereceiver.reinforcer == reinforcer ? 2 : 0) + ) + .onTapGesture { + self.robot.run(reinforcer) + self.carereceiver.reinforcer = reinforcer + } + } + } + .animation(.default, value: self.carereceiver.reinforcer) + } + + // MARK: Private + + private let robot: Robot = .shared + + @ObservedObject private var styleManager = StyleManager.shared +} + +// MARK: - l10n.ReinforcerPicker + +extension l10n { + enum ReinforcerPicker { + static let header = LocalizedString("lekaapp.reinforcer_picker.header", value: "Reinforcer choice", comment: "Reinforcer picker header") + + static let description = LocalizedString( + "lekaapp.reinforcer_picker.avatar_choice_button", + value: """ + Reinforcer is a repetitive light effect from the robot that you can activate to reward the user's behavior. + If your robot is connected, you can test the reinforcers before choosing one. + """, + + comment: " Reinforcer picker description" + ) + } +} + +#Preview { + ReinforcerPicker(carereceiver: .constant(Carereceiver())) +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Settings/SettingsView+l10n.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Settings/SettingsView+l10n.swift new file mode 100644 index 0000000000..1346f86f7a --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Settings/SettingsView+l10n.swift @@ -0,0 +1,75 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +// swiftlint:disable line_length nesting + +extension l10n { + enum SettingsView { + enum CredentialsSection { + enum ChangeCredentials { + static let buttonLabel = LocalizedString("settings_view.credentials_section.change_credentials.button_label", value: "Contact Support for Email/Password Change", comment: "Change credentials button label") + + static let alertTitle = LocalizedString("settings_view.credentials_section.change_credentials.alert_title", + value: "Need to Update Your Credentials?", + comment: "Change credentials alert title") + + static let alertMessage = LocalizedString("settings_view.credentials_section.change_credentials.alert_message", + value: """ + For security reasons, changes to your email or password need to be handled by our support team. + Please contact us at + support@leka.io + and we'll be happy to assist you with updating your account information. + """, + comment: "Change credentials alert message") + } + + static let emailLabel = LocalizedString("settings_view.credentials_section.email_label", value: "Account Email", comment: "Account email address label") + } + + enum AccountSection { + enum LogOut { + static let alertTitle = LocalizedString("settings_view.account_section.log_out.alert_title", value: "Confirm Logout", comment: "Log out alert title") + + static let buttonLabel = LocalizedString("settings_view.account_section.log_out.button_label", value: "Log Out", comment: "Log out button label") + + static let alertMessage = LocalizedString("settings_view.account_section.log_out.alert_message", value: "Are you sure you want to log out? You will need to enter your email and password to log back in.", comment: "Log out alert message") + + static let alertButtonLabel = LocalizedString("settings_view.account_section.log_out.alert_buton_label", value: "Log out", comment: "Log out alert button label") + + static let errorAlertTitle = LocalizedString("settings_view.account_section.log_out.error.alert_title", value: "Logout Error", comment: "Log out error alert title") + static let errorAlertMessage = LocalizedString("settings_view.account_section.log_out.error.alert_message", value: "We encountered an issue logging you out. Please try again. If the problem persists, contact our support team for assistance.", comment: "Log out error alert message") + } + + enum DeleteAccount { + static let buttonLabel = LocalizedString("settings_view.account_section.delete_account.button_label", value: "Delete Account", comment: "Delete account button label") + + static let alertTitle = LocalizedString("settings_view.account_section.delete_account.alert_title", value: "Delete Account?", comment: "Delete account alert title") + + static let alertMessage = LocalizedString("settings_view.account_section.delete_account.alert_message", + value: """ + To delete your account, please contact our support team at + support@leka.io + We're here to help you with the process and answer any questions you might have. + """, + comment: "Delete account alert message") + } + } + + enum ProfilesSection { + static let switchProfileButtonLabel = LocalizedString("settings_view.profiles_section.switch_profiles_button_label", value: "Switch Caregiver Profile", comment: "Switch caregiver profile button label") + } + + enum ChangeLanguageSection { + static let buttonLabel = LocalizedString("settings_view.change_language_section.button_label", value: "Change App Language", comment: "Change app language button label") + } + + static let navigationTitle = LocalizedString("settings_view.navigation_title", value: "Settings", comment: "The navigation title of Settings View") + + static let closeButtonLabel = LocalizedString("settings_view.close_button_label", value: "Close", comment: "Close button label of Settings View") + } +} + +// swiftlint:enable line_length nesting diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Settings/SettingsView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Settings/SettingsView.swift new file mode 100644 index 0000000000..b60f31d01a --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Settings/SettingsView.swift @@ -0,0 +1,143 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - SettingsView + +struct SettingsView: View { + @Environment(\.openURL) private var openURL + @Environment(\.dismiss) var dismiss + + @State private var showConfirmCredentialsChange: Bool = false + @State private var showConfirmDisconnection: Bool = false + @State private var showConfirmDeleteAccount: Bool = false + + var body: some View { + Form { + Section { + Button { + self.dismiss() + self.navigation.sheetContent = .caregiverPicker + } label: { + Label(String(l10n.SettingsView.ProfilesSection.switchProfileButtonLabel.characters), systemImage: "person.2.gobackward") + } + } + + Section { + Button(String(l10n.SettingsView.ChangeLanguageSection.buttonLabel.characters), systemImage: "globe") { + guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { + return + } + + self.openURL(settingsURL) + } + } + + Section { + LabeledContent { + Text(self.authManager.currentUserEmail ?? "") + .multilineTextAlignment(.trailing) + .foregroundStyle(Color.secondary) + } label: { + Text(l10n.SettingsView.CredentialsSection.emailLabel) + } + } footer: { + Button { + self.showConfirmCredentialsChange = true + } label: { + HStack { + Spacer() + Text(l10n.SettingsView.CredentialsSection.ChangeCredentials.buttonLabel) + .font(.footnote) + } + } + .alert(String(l10n.SettingsView.CredentialsSection.ChangeCredentials.alertTitle.characters), + isPresented: self.$showConfirmCredentialsChange) {} message: { + Text(l10n.SettingsView.CredentialsSection.ChangeCredentials.alertMessage) + } + } + + Section { + Button { + self.showConfirmDisconnection = true + } label: { + Label(String(l10n.SettingsView.AccountSection.LogOut.buttonLabel.characters), + systemImage: "rectangle.portrait.and.arrow.forward") + } + .alert(String(l10n.SettingsView.AccountSection.LogOut.alertTitle.characters), + isPresented: self.$showConfirmDisconnection) + { + Button(role: .destructive) { + self.dismiss() + self.authManager.signOut() + self.reset() + } label: { + Text(l10n.SettingsView.AccountSection.LogOut.alertButtonLabel) + } + } message: { + Text(l10n.SettingsView.AccountSection.LogOut.alertMessage) + } + .alert(String(l10n.SettingsView.AccountSection.LogOut.errorAlertTitle.characters), + isPresented: self.$authManagerViewModel.showErrorAlert) + { + Button("OK", role: .cancel) {} + } message: { + Text(l10n.SettingsView.AccountSection.LogOut.errorAlertMessage) + } + + Button(role: .destructive) { + self.showConfirmDeleteAccount = true + } label: { + Label(String(l10n.SettingsView.AccountSection.DeleteAccount.buttonLabel.characters), systemImage: "trash") + .foregroundStyle(.red) + } + .alert(String(l10n.SettingsView.AccountSection.DeleteAccount.alertTitle.characters), + isPresented: self.$showConfirmDeleteAccount) + { + Button("OK", role: .cancel) {} + } message: { + Text(l10n.SettingsView.AccountSection.DeleteAccount.alertMessage) + } + } + } + .navigationTitle(String(l10n.SettingsView.navigationTitle.characters)) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(String(l10n.SettingsView.closeButtonLabel.characters)) { + self.dismiss() + } + } + } + + .preferredColorScheme(self.styleManager.colorScheme) + } + + private let authManager = AuthManager.shared + private let caregiverManager: CaregiverManager = .shared + private let carereceiverManager: CarereceiverManager = .shared + + @ObservedObject private var authManagerViewModel = AuthManagerViewModel.shared + @ObservedObject private var styleManager: StyleManager = .shared + @ObservedObject private var navigation: Navigation = .shared + + private func reset() { + self.caregiverManager.resetData() + self.carereceiverManager.resetData() + self.styleManager.accentColor = DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor + self.styleManager.colorScheme = .light + } +} + +#Preview { + Text("Preview") + .sheet(isPresented: .constant(true)) { + NavigationStack { + SettingsView() + } + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/TextFields/Focusable.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/TextFields/Focusable.swift new file mode 100644 index 0000000000..6b70ed5e05 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/TextFields/Focusable.swift @@ -0,0 +1,10 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +enum Focusable { + case email + case password +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/TextFields/TextFieldDefault.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/TextFields/TextFieldDefault.swift new file mode 100644 index 0000000000..4cc15561a1 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/TextFields/TextFieldDefault.swift @@ -0,0 +1,27 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - TextFieldDefault + +struct TextFieldDefault: View { + let label: String + @Binding var entry: String + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text(self.label) + + TextField("", text: self.$entry) + .textFieldStyle(.roundedBorder) + .autocorrectionDisabled() + } + } +} + +#Preview { + TextFieldDefault(label: "Name", entry: .constant("Gaëtan")) +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/TextFields/TextFieldEmail.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/TextFields/TextFieldEmail.swift new file mode 100644 index 0000000000..40e2a32c06 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/TextFields/TextFieldEmail.swift @@ -0,0 +1,70 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import Combine +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - l10n.TextFieldEmail + +// swiftlint:disable line_length + +extension l10n { + enum TextFieldEmail { + static let label = LocalizedString("lekaapp.TextFieldEmail.label", value: "Email", comment: "TextFieldEmail label") + + static let invalidEmailErrorLabel = LocalizedString("lekaapp.TextFieldEmail.invalidEmailErrorLabel", value: "The email is not valid", comment: "TextFieldEmail invalid Email Error Label") + } +} + +// MARK: - TextFieldEmail + +// swiftlint:enable line_length + +struct TextFieldEmail: View { + // MARK: Internal + + @Binding var entry: String + @FocusState var focused: Focusable? + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text(l10n.TextFieldEmail.label) + + TextField("", text: self.$entry) + .textFieldStyle(.roundedBorder) + .keyboardType(.emailAddress) + .textContentType(.emailAddress) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .onReceive(Just(self.entry)) { newValue in + if self.focused != .email { + self.entry = newValue.trimmingCharacters(in: .whitespaces) + } + } + .onAppear { self.focused = .email } + .focused(self.$focused, equals: .email) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(self.focused == .email ? .blue : .clear, lineWidth: 1) + ) + + Text(l10n.TextFieldEmail.invalidEmailErrorLabel) + .font(.footnote) + .foregroundStyle(self.isErrorMessageVisible ? .red : .clear) + } + } + + // MARK: Private + + private var isErrorMessageVisible: Bool { + self.entry.isInvalidEmail(checkEmpty: false) && !self.entry.isEmpty && self.focused != .email + } +} + +#Preview { + TextFieldEmail(entry: .constant("john.doe@mail.com")) +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/TextFields/TextFieldPassword.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/TextFields/TextFieldPassword.swift new file mode 100644 index 0000000000..ec0f4a07cc --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/TextFields/TextFieldPassword.swift @@ -0,0 +1,80 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import Combine +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - l10n.TextFieldPassword + +// swiftlint:disable line_length + +extension l10n { + enum TextFieldPassword { + static let label = LocalizedString("lekaapp.TextFieldPassword.label", value: "Password", comment: "TextFieldPassword label") + + static let invalidPasswordErrorLabel = LocalizedString("lekaapp.TextFieldPassword.invalidPasswordErrorLabel", value: "12 characters minimum, no spaces", comment: "TextFieldPassword invalid Password Error Label") + } +} + +// MARK: - TextFieldPassword + +// swiftlint:enable line_length + +struct TextFieldPassword: View { + // MARK: Internal + + @Binding var entry: String + @FocusState var focused: Focusable? + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text(l10n.TextFieldPassword.label) + + HStack { + Group { + if self.isSecured { + SecureField("", text: self.$entry) + } else { + TextField("", text: self.$entry) + } + } + .textFieldStyle(.roundedBorder) + .textContentType(.password) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .onReceive(Just(self.entry)) { newValue in + self.entry = newValue.trimmingCharacters(in: .whitespaces) + } + .focused(self.$focused, equals: .password) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(self.focused == .password ? .blue : .clear, lineWidth: 1) + ) + + Button("", systemImage: self.isSecured ? "eye" : "eye.slash") { + self.isSecured.toggle() + } + } + + if self.authManagerViewModel.userAction == .userIsSigningUp { + Text(l10n.TextFieldPassword.invalidPasswordErrorLabel) + .font(.footnote) + .lineLimit(2) + .foregroundStyle(self.entry.isValidPassword() ? .green : .gray) + } + } + } + + // MARK: Private + + @ObservedObject private var authManagerViewModel = AuthManagerViewModel.shared + @State private var isSecured: Bool = true +} + +#Preview { + TextFieldPassword(entry: .constant("")) +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/CarereceiverAvatarCell.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/CarereceiverAvatarCell.swift new file mode 100644 index 0000000000..392893d362 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/CarereceiverAvatarCell.swift @@ -0,0 +1,71 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import SwiftUI + +struct CarereceiverAvatarCell: View { + // MARK: Lifecycle + + init(carereceiver: Carereceiver, isSelected: Bool = false) { + self.carereceiver = carereceiver + self.isSelected = isSelected + } + + // MARK: Internal + + let carereceiver: Carereceiver + var isSelected: Bool + + var body: some View { + VStack(spacing: 10) { + Image(uiImage: Avatars.iconToUIImage(icon: self.carereceiver.avatar)) + .resizable() + .aspectRatio(contentMode: .fit) + .background(DesignKitAsset.Colors.blueGray.swiftUIColor) + .clipShape(Circle()) + .overlay { + Circle() + .stroke(self.styleManager.accentColor!, lineWidth: self.isSelected ? 5 : 0) + } + .overlay { + Circle() + .strokeBorder(self.strokeColor, lineWidth: 2) + .background { + Circle() + .fill(Color(uiColor: UIColor.systemGray6)) + } + .overlay { + Image(uiImage: self.carereceiver.reinforcer.image) + .resizable() + .aspectRatio(contentMode: .fit) + .padding(5) + } + .frame(maxWidth: 60) + .offset(x: 60, y: 40) + } + .frame(maxWidth: 120) + + Text(self.carereceiver.username) + .font(.headline) + .lineLimit(2, reservesSpace: true) + } + } + + // MARK: Private + + private let strokeColor: Color = .init(light: UIColor.systemGray3, dark: UIColor.systemGray2) + @ObservedObject private var styleManager: StyleManager = .shared +} + +#Preview { + CarereceiverAvatarCell( + carereceiver: Carereceiver( + username: "Chantal", + avatar: Avatars.categories[2].avatars[4], + reinforcer: .fire + ) + ) +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/CarereceiverList.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/CarereceiverList.swift new file mode 100644 index 0000000000..209b141ade --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/CarereceiverList.swift @@ -0,0 +1,133 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - CarereceiverList + +struct CarereceiverList: View { + // MARK: Internal + + var body: some View { + VStack { + ScrollView(showsIndicators: true) { + HStack(alignment: .center, spacing: 30) { + Image(systemName: "figure.2.arms.open") + .resizable() + .scaledToFit() + .frame(width: 80, height: 80) + .foregroundStyle(self.styleManager.accentColor!) + + VStack(alignment: .leading) { + Text(l10n.CarereceiverList.title) + .font(.largeTitle) + .fontWeight(.bold) + + Text(l10n.CarereceiverList.subtitle) + .font(.title2) + + Text(l10n.CarereceiverList.description) + .foregroundStyle(.secondary) + + Divider() + .padding(.top) + } + + Spacer() + } + .padding(.horizontal) + .padding() + + if self.carereceiverManagerViewModel.carereceivers.isEmpty { + VStack { + Text(l10n.CarereceiverList.AddFirstCarereceiver.message) + .font(.title2) + .multilineTextAlignment(.center) + + Button { + self.isCarereceiverCreationPresented = true + } label: { + Text(l10n.CarereceiverList.AddFirstCarereceiver.addButtonLabel) + } + .buttonStyle(.borderedProminent) + } + .padding(.top, 150) + } else { + LazyVGrid(columns: self.columns, spacing: 40) { + ForEach(self.carereceiverManagerViewModel.carereceivers) { carereceiver in + NavigationLink(value: carereceiver) { + CarereceiverAvatarCell(carereceiver: carereceiver) + } + } + } + } + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + self.isCarereceiverCreationPresented = true + } label: { + Text(l10n.CarereceiverList.addButtonLabel) + } + } + } + .sheet(isPresented: self.$isCarereceiverCreationPresented) { + NavigationStack { + CreateCarereceiverView() + .navigationBarTitleDisplayMode(.inline) + } + } + .navigationDestination(for: Carereceiver.self) { carereceiver in + CarereceiverView(carereceiver: carereceiver) + } + } + } + + // MARK: Private + + private let columns = Array(repeating: GridItem(), count: 4) + + @StateObject private var carereceiverManagerViewModel = CarereceiverManagerViewModel() + @ObservedObject private var styleManager: StyleManager = .shared + @State private var isCarereceiverCreationPresented: Bool = false +} + +// MARK: - l10n.CarereceiverList + +extension l10n { + enum CarereceiverList { + enum AddFirstCarereceiver { + static let message = LocalizedString("lekaapp.carereceiver_list.add_first_carereceiver.message", + value: "No care receiver profiles have been created yet.", + comment: "Carereceiver list add first carereceiver message") + + static let addButtonLabel = LocalizedString("lekaapp.carereceiver_list.add_first_carereceiver.add_button_label", + value: "Add your first care receiver profile", + comment: "Carereceiver list add first carereceiver button label") + } + + static let title = LocalizedString("lekaapp.carereceiver_list.title", + value: "Care receivers", + comment: "Carereceiver list title") + + static let subtitle = LocalizedString("lekaapp.carereceiver_list.subtitle", + value: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + comment: "Carereceiver list subtitle") + + static let description = LocalizedString("lekaapp.carereceiver_list.description", + value: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", + comment: "Carereceiver list description") + + static let addButtonLabel = LocalizedString("lekaapp.carereceiver_list.add_button_label", + value: "Add profile", + comment: "Carereceiver list add button label") + } +} + +#Preview { + CarereceiverList() +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/CarereceiverPicker.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/CarereceiverPicker.swift new file mode 100644 index 0000000000..86194fc5eb --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/CarereceiverPicker.swift @@ -0,0 +1,181 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - CarereceiverPicker + +struct CarereceiverPicker: View { + // MARK: Lifecycle + + init(selected: Carereceiver? = nil, onDismiss: (() -> Void)? = nil, onSelected: ((Carereceiver) -> Void)? = nil, onSkip: (() -> Void)? = nil) { + self.selectedCarereceiver = selected + self.onDismiss = onDismiss + self.onSelected = onSelected + self.onSkip = onSkip + } + + // MARK: Internal + + @Environment(\.dismiss) var dismiss + + var onDismiss: (() -> Void)? + var onSelected: ((Carereceiver) -> Void)? + var onSkip: (() -> Void)? + + var body: some View { + VStack { + if self.carereceiverManagerViewModel.carereceivers.isEmpty { + VStack { + Text(l10n.CarereceiverPicker.AddFirstCarereceiver.message) + .font(.title2) + .multilineTextAlignment(.center) + + Button { + self.dismiss() + self.navigation.selectedCategory = .carereceivers + } label: { + Text(l10n.CarereceiverPicker.AddFirstCarereceiver.buttonLabel) + } + .buttonStyle(.borderedProminent) + } + } else { + ScrollView(showsIndicators: true) { + LazyVGrid(columns: self.columns, spacing: 40) { + ForEach(self.carereceiverManagerViewModel.carereceivers) { carereceiver in + CarereceiverAvatarCell(carereceiver: carereceiver, isSelected: self.selectedCarereceiver == carereceiver) + .onTapGesture { + withAnimation(.default) { + if self.selectedCarereceiver == carereceiver { + self.selectedCarereceiver = nil + } else { + self.selectedCarereceiver = carereceiver + } + } + } + } + } + .padding() + } + } + } + .navigationTitle(String(l10n.CarereceiverPicker.title.characters)) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button { + self.action = .dismiss + self.dismiss() + } label: { + Text(l10n.CarereceiverPicker.closeButtonLabel) + } + } + ToolbarItem(placement: .topBarTrailing) { + Button { + self.action = .select + self.dismiss() + } label: { + Text(l10n.CarereceiverPicker.validateButtonLabel) + } + .disabled(self.selectedCarereceiver == nil) + } + ToolbarItemGroup(placement: .bottomBar) { + Button { + self.action = .skip + self.dismiss() + } label: { + Text(l10n.CarereceiverPicker.skipButtonLabel) + .font(.footnote) + } + Spacer() + } + } + .onDisappear { + switch self.action { + case .dismiss: + self.onDismiss?() + case .select: + if let selectedCarereceiver = self.selectedCarereceiver { + self.onSelected?(selectedCarereceiver) + } + case .skip: + self.onSkip?() + case .none: + break + } + + self.action = nil + } + } + + // MARK: Private + + private enum ActionType { + case dismiss + case select + case skip + } + + private let columns = Array(repeating: GridItem(), count: 4) + + @StateObject private var carereceiverManagerViewModel = CarereceiverManagerViewModel() + @ObservedObject private var navigation: Navigation = .shared + @State private var selectedCarereceiver: Carereceiver? + @State private var action: ActionType? +} + +// MARK: - l10n.CarereceiverPicker + +extension l10n { + // swiftlint:disable nesting + enum CarereceiverPicker { + enum AddFirstCarereceiver { + static let message = LocalizedString("lekaapp.carereceiver_picker.add_first_carereceiver.message", + value: """ + No care receiver profiles have been created yet. + You can create one in the Care Receivers section. + """, + comment: "Carereceiver picker add first carereceiver message") + + static let buttonLabel = LocalizedString("lekaapp.carereceiver_picker.add_first_carereceiver.add_button_label", + value: "Go to care receiver section", + comment: "Carereceiver picker add first carereceiver button label") + } + + static let title = LocalizedString("lekaapp.carereceiver_picker.title", + value: "Who do you do this activity with?", + comment: "Carereceiver picker title") + + static let validateButtonLabel = LocalizedString("lekaapp.carereceiver_picker.validate_button_label", + value: "Validate", + comment: "Carereceiver picker validate button label") + + static let skipButtonLabel = LocalizedString("lekaapp.carereceiver_picker.skip_button_label", + value: "Continue without profile", + comment: "Carereceiver picker continue without profile button label") + + static let closeButtonLabel = LocalizedString("lekaapp.carereceiver_picker.close_button_label", + value: "Close", + comment: "Carereceiver picker close button label") + } + // swiftlint:enable nesting +} + +#Preview { + Text("Preview") + .sheet(isPresented: .constant(true)) { + NavigationStack { + CarereceiverPicker(onDismiss: { + print("dismiss") + }, onSelected: { + print("selected carereceiver: \($0)") + }, + onSkip: { + print("skip") + }) + } + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/CreateCarereceiverView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/CreateCarereceiverView.swift new file mode 100644 index 0000000000..95d0537e33 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/CreateCarereceiverView.swift @@ -0,0 +1,191 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import Combine +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - CreateCarereceiverViewModel + +class CreateCarereceiverViewModel: ObservableObject { + // MARK: Internal + + var carereceiverManager: CarereceiverManager = .shared + + // MARK: - Public functions + + func createCarereceiver(carereceiver: Carereceiver, onCreated: @escaping (Carereceiver) -> Void, onError: @escaping (Error) -> Void) { + self.carereceiverManager.createCarereceiver(carereceiver: carereceiver) + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + print("Carereceiver Creation successful.") + case let .failure(error): + print("Carereceiver Creation failed with error: \(error)") + onError(error) + } + }, receiveValue: { createdCarereceiver in + onCreated(createdCarereceiver) + }) + .store(in: &self.cancellables) + } + + // MARK: Private + + private var cancellables = Set() +} + +// MARK: - CreateCarereceiverView + +struct CreateCarereceiverView: View { + // MARK: Lifecycle + + init(onCancel: (() -> Void)? = nil, onCreated: ((Carereceiver) -> Void)? = nil) { + self.onCancel = onCancel + self.onCreated = onCreated + } + + // MARK: Internal + + @Environment(\.dismiss) var dismiss + var onCancel: (() -> Void)? + var onCreated: ((Carereceiver) -> Void)? + + var carereceiverManager: CarereceiverManager = .shared + + var body: some View { + VStack(spacing: 40) { + Form { + Section { + self.avatarPickerButton + .buttonStyle(.borderless) + .listRowBackground(Color.clear) + } + + Section { + LabeledContent(String(l10n.CarereceiverCreation.carereceiverNameLabel.characters)) { + TextField("", text: self.$newCarereceiver.username) + .textContentType(.username) + .textInputAutocapitalization(.words) + .autocorrectionDisabled() + .multilineTextAlignment(.trailing) + .foregroundStyle(Color.secondary) + } + } + + Button(String(l10n.CarereceiverCreation.registerProfilButton.characters)) { + if self.newCarereceiver.avatar.isEmpty { + self.newCarereceiver.avatar = Avatars.categories.first!.avatars.randomElement()! + } + self.viewModel.createCarereceiver(carereceiver: self.newCarereceiver, onCreated: { createdCarereceiver in + self.newCarereceiver = createdCarereceiver + withAnimation { + self.action = .created + self.dismiss() + } + }, onError: { error in + // Handle error + print(error.localizedDescription) + }) + } + .disabled(self.newCarereceiver.username.isEmpty) + .buttonStyle(.borderedProminent) + .listRowBackground(Color.clear) + .frame(maxWidth: .infinity, alignment: .center) + } + } + .navigationTitle(String(l10n.CarereceiverCreation.title.characters)) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button { + self.action = .cancel + self.dismiss() + } label: { + Image(systemName: "xmark.circle") + } + } + } + .onDisappear { + switch self.action { + case .cancel: + self.onCancel?() + case .created: + self.onCreated?(self.newCarereceiver) + case .none: + break + } + + self.action = nil + } + } + + // MARK: Private + + private enum ActionType { + case cancel + case created + } + + @StateObject private var viewModel = CreateCarereceiverViewModel() + + @State private var newCarereceiver = Carereceiver() + @State private var isAvatarPickerPresented: Bool = false + @State private var action: ActionType? + @State private var cancellables = Set() + + private var avatarPickerButton: some View { + Button { + self.isAvatarPickerPresented = true + } label: { + VStack(alignment: .center, spacing: 15) { + AvatarPicker.ButtonLabel(image: self.newCarereceiver.avatar) + Text(l10n.CarereceiverCreation.avatarChoiceButton) + .font(.headline) + } + } + .sheet(isPresented: self.$isAvatarPickerPresented) { + NavigationStack { + AvatarPicker(selectedAvatar: self.newCarereceiver.avatar, + onValidate: { avatar in + self.newCarereceiver.avatar = avatar + }) + .navigationBarTitleDisplayMode(.inline) + } + } + .frame(maxWidth: .infinity, alignment: .center) + } +} + +// MARK: - l10n.CarereceiverCreation + +// swiftlint:disable line_length + +extension l10n { + enum CarereceiverCreation { + static let title = LocalizedString("lekaapp.carereceiver_creation.title", value: "Create a carereceiver profile", comment: " Carereceiver creation title") + + static let avatarChoiceButton = LocalizedString("lekaapp.carereceiver_creation.avatar_choice_button", value: "Choose an avatar", comment: " Carereceiver creation avatar choice button label") + + static let carereceiverNameLabel = LocalizedString("lekaapp.carereceiver_creation.carereceiver_name_label", value: "Username", comment: " Carereceiver creation carereceiver name textfield label") + + static let registerProfilButton = LocalizedString("lekaapp.carereceiver_creation.register_profil_button", value: "Register profile", comment: " Carereceiver creation register profil button label") + } +} + +// swiftlint:enable line_length + +#Preview { + Text("Preview") + .sheet(isPresented: .constant(true)) { + NavigationStack { + CreateCarereceiverView(onCancel: { + print("Care receiver creation canceled") + }, onCreated: { + print("Carereceiver \($0.username) created") + }) + } + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/EditCarereceiverView+l10n.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/EditCarereceiverView+l10n.swift new file mode 100644 index 0000000000..2ef5c211cb --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/EditCarereceiverView+l10n.swift @@ -0,0 +1,19 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +// swiftlint:disable line_length + +extension l10n { + enum EditCarereceiverView { + static let navigationTitle = LocalizedString("edit_carereceiver_view.navigation_title", value: "Edit profil", comment: "The navigation title of Edit Carereceiver View") + + static let saveButtonLabel = LocalizedString("edit_carereceiver_view.save_button_label", value: "Save", comment: "Save button label of Edit Carereceiver View") + + static let closeButtonLabel = LocalizedString("edit_carereceiver_view.close_button_label", value: "Close", comment: "Close button label of Edit Carereceiver View") + } +} + +// swiftlint:enable line_length diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/EditCarereceiverView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/EditCarereceiverView.swift new file mode 100644 index 0000000000..070aeed8b4 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/CareReceiver/EditCarereceiverView.swift @@ -0,0 +1,98 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - EditCarereceiverView + +struct EditCarereceiverView: View { + // MARK: Internal + + @Environment(\.dismiss) var dismiss + @Binding var modifiedCarereceiver: Carereceiver + + var body: some View { + VStack(spacing: 40) { + Form { + Section { + self.avatarPickerButton + .buttonStyle(.borderless) + .listRowBackground(Color.clear) + } + + Section { + LabeledContent(String(l10n.CarereceiverCreation.carereceiverNameLabel.characters)) { + TextField("", text: self.$modifiedCarereceiver.username) + .textContentType(.username) + .textInputAutocapitalization(.words) + .autocorrectionDisabled() + .multilineTextAlignment(.trailing) + .foregroundStyle(Color.secondary) + } + } + + Section { + LabeledContent(String(l10n.ReinforcerPicker.header.characters)) { + ReinforcerPicker(carereceiver: self.$modifiedCarereceiver) + } + } footer: { + Text(l10n.ReinforcerPicker.description) + } + } + } + .navigationTitle(String(l10n.EditCarereceiverView.navigationTitle.characters)) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(String(l10n.EditCarereceiverView.closeButtonLabel.characters)) { + self.dismiss() + } + } + ToolbarItem(placement: .topBarTrailing) { + Button(String(l10n.EditCarereceiverView.saveButtonLabel.characters)) { + self.carereceiverManager.updateCarereceiver(carereceiver: &self.modifiedCarereceiver) + self.dismiss() + } + } + } + } + + // MARK: Private + + @State private var isAvatarPickerPresented: Bool = false + var carereceiverManager: CarereceiverManager = .shared + + private var avatarPickerButton: some View { + Button { + self.isAvatarPickerPresented = true + } label: { + VStack(alignment: .center, spacing: 15) { + AvatarPicker.ButtonLabel(image: self.modifiedCarereceiver.avatar) + Text(l10n.CarereceiverCreation.avatarChoiceButton) + .font(.headline) + } + } + .sheet(isPresented: self.$isAvatarPickerPresented) { + NavigationStack { + AvatarPicker(selectedAvatar: self.modifiedCarereceiver.avatar, + onValidate: { avatar in + self.modifiedCarereceiver.avatar = avatar + }) + .navigationBarTitleDisplayMode(.inline) + } + } + .frame(maxWidth: .infinity, alignment: .center) + } +} + +#Preview { + Text("Preview") + .sheet(isPresented: .constant(true)) { + NavigationStack { + EditCarereceiverView(modifiedCarereceiver: .constant(Carereceiver())) + } + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/CaregiverAvatarCell.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/CaregiverAvatarCell.swift new file mode 100644 index 0000000000..9cead84d6b --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/CaregiverAvatarCell.swift @@ -0,0 +1,38 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import SwiftUI + +struct CaregiverAvatarCell: View { + let caregiver: Caregiver + + var body: some View { + VStack(spacing: 10) { + Image(uiImage: Avatars.iconToUIImage(icon: self.caregiver.avatar)) + .resizable() + .aspectRatio(contentMode: .fit) + .background(DesignKitAsset.Colors.blueGray.swiftUIColor) + .clipShape(Circle()) + + Text("\(self.caregiver.firstName) \(self.caregiver.lastName)") + .font(.headline) + .lineLimit(2, reservesSpace: true) + } + } +} + +#Preview { + CaregiverAvatarCell( + caregiver: Caregiver( + firstName: "Chantal", + lastName: "Goya", + avatar: Avatars.categories[2].avatars[4], + professions: [], + colorScheme: .dark, + colorTheme: .orange + ) + ) +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/CaregiverPicker.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/CaregiverPicker.swift new file mode 100644 index 0000000000..ebad32db45 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/CaregiverPicker.swift @@ -0,0 +1,124 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - CaregiverPicker + +struct CaregiverPicker: View { + // MARK: Internal + + @Environment(\.dismiss) var dismiss + + var body: some View { + VStack { + if self.caregiverManagerViewModel.caregivers.isEmpty { + VStack { + Text(l10n.CaregiverPicker.AddFirstCaregiver.message) + .font(.title2) + .multilineTextAlignment(.center) + + Button { + self.isCaregiverCreationPresented = true + } label: { + Text(l10n.CaregiverPicker.AddFirstCaregiver.buttonLabel) + } + .buttonStyle(.borderedProminent) + } + } else { + ScrollView(showsIndicators: false) { + LazyVGrid(columns: self.columns, spacing: 40) { + ForEach(self.caregiverManagerViewModel.caregivers, id: \.id) { caregiver in + Button { + self.styleManager.colorScheme = caregiver.colorScheme + self.styleManager.accentColor = caregiver.colorTheme.color + self.caregiverManager.setCurrentCaregiver(to: caregiver) + self.dismiss() + } label: { + CaregiverAvatarCell(caregiver: caregiver) + .frame(maxWidth: 140) + } + } + } + } + .padding() + } + } + .padding(.horizontal, 50) + .navigationTitle(String(l10n.CaregiverPicker.title.characters)) + .sheet(isPresented: self.$isCaregiverCreationPresented) { + NavigationStack { + CreateCaregiverView() + .navigationBarTitleDisplayMode(.inline) + } + } + .toolbar { + if self.caregiverManagerViewModel.currentCaregiver != nil { + ToolbarItem(placement: .topBarLeading) { + Button { + self.dismiss() + } label: { + Text(l10n.CaregiverPicker.closeButtonLabel) + } + } + } + ToolbarItem(placement: .topBarTrailing) { + Button { + self.isCaregiverCreationPresented = true + } label: { + Text(l10n.CaregiverPicker.addButtonLabel) + } + } + } + } + + // MARK: Private + + @ObservedObject private var authManagerViewModel: AuthManagerViewModel = .shared + @ObservedObject private var styleManager: StyleManager = .shared + + @StateObject private var caregiverManagerViewModel = CaregiverManagerViewModel() + + @State private var selected: String = "" + @State private var isCaregiverCreationPresented: Bool = false + + private var caregiverManager: CaregiverManager = .shared + private let columns = Array(repeating: GridItem(), count: 4) +} + +// MARK: - l10n.CaregiverPicker + +extension l10n { + // swiftlint:disable line_length nesting + enum CaregiverPicker { + enum AddFirstCaregiver { + static let message = LocalizedString("lekaapp.caregiver_picker.add_first_caregiver.message", + value: "No caregiver profiles have been created yet.", + comment: "Caregiver picker add first caregiver message") + + static let buttonLabel = LocalizedString("lekaapp.caregiver_picker.add_first_caregiver.add_button_label", + value: "Add your first caregiver profile", + comment: "Caregiver picker add first caregiver button label") + } + + static let title = LocalizedString("lekaapp.caregiver_picker.title", value: "Who are you ?", comment: "Caregiver picker title") + + static let addButtonLabel = LocalizedString("lekaapp.caregiver_picker.add_button_label", value: "Add profile", comment: "Caregiver picker add button label") + + static let closeButtonLabel = LocalizedString("lekaapp.caregiver_picker.close_button_label", value: "Close", comment: "Caregiver picker close button label") + } + // swiftlint:enable line_length nesting +} + +#Preview { + Text("Preview") + .sheet(isPresented: .constant(true)) { + NavigationStack { + CaregiverPicker() + } + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/CreateCaregiverView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/CreateCaregiverView.swift new file mode 100644 index 0000000000..aa7998edf8 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/CreateCaregiverView.swift @@ -0,0 +1,207 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import Combine +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - CreateCaregiverViewModel + +class CreateCaregiverViewModel: ObservableObject { + // MARK: Internal + + var caregiverManager: CaregiverManager = .shared + + // MARK: - Public functions + + func createCaregiver(caregiver: Caregiver, onCreated: @escaping (Caregiver) -> Void, onError: @escaping (Error) -> Void) { + self.caregiverManager.createCaregiver(caregiver: caregiver) + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + print("Caregiver Creation successful.") + case let .failure(error): + print("Caregiver Creation failed with error: \(error)") + onError(error) + } + }, receiveValue: { createdCaregiver in + onCreated(createdCaregiver) + }) + .store(in: &self.cancellables) + } + + // MARK: Private + + private var cancellables = Set() +} + +// MARK: - CreateCaregiverView + +struct CreateCaregiverView: View { + // MARK: Lifecycle + + init(onCancel: (() -> Void)? = nil, onCreated: ((Caregiver) -> Void)? = nil) { + self.onCancel = onCancel + self.onCreated = onCreated + } + + // MARK: Internal + + @Environment(\.dismiss) var dismiss + var onCancel: (() -> Void)? + var onCreated: ((Caregiver) -> Void)? + + var caregiverManager: CaregiverManager = .shared + + var body: some View { + VStack(spacing: 40) { + Form { + Section { + self.avatarPickerButton + .buttonStyle(.borderless) + .listRowBackground(Color.clear) + } + + Section { + LabeledContent(String(l10n.CaregiverCreation.caregiverFirstNameLabel.characters)) { + TextField("", text: self.$newCaregiver.firstName) + .textContentType(.givenName) + .textInputAutocapitalization(.words) + .autocorrectionDisabled() + .multilineTextAlignment(.trailing) + .foregroundStyle(Color.secondary) + } + LabeledContent(String(l10n.CaregiverCreation.caregiverLastNameLabel.characters)) { + TextField("", text: self.$newCaregiver.lastName) + .textContentType(.familyName) + .textInputAutocapitalization(.words) + .autocorrectionDisabled() + .multilineTextAlignment(.trailing) + .foregroundStyle(Color.secondary) + } + } + + Section { + ProfessionListView(caregiver: self.$newCaregiver) + .navigationBarTitleDisplayMode(.inline) + } + + Button(String(l10n.CaregiverCreation.registerProfilButton.characters)) { + if self.newCaregiver.avatar.isEmpty { + self.newCaregiver.avatar = Avatars.categories.first!.avatars.randomElement()! + } + self.viewModel.createCaregiver(caregiver: self.newCaregiver, onCreated: { createdCaregiver in + self.newCaregiver = createdCaregiver + withAnimation { + self.action = .created + self.dismiss() + } + }, onError: { error in + // Handle error + print(error.localizedDescription) + }) + } + .disabled(self.newCaregiver.firstName.isEmpty) + .buttonStyle(.borderedProminent) + .listRowBackground(Color.clear) + .frame(maxWidth: .infinity, alignment: .center) + } + } + .navigationTitle(String(l10n.CaregiverCreation.title.characters)) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button { + self.action = .cancel + self.dismiss() + } label: { + Image(systemName: "xmark.circle") + } + } + } + .onDisappear { + switch self.action { + case .cancel: + self.onCancel?() + case .created: + self.onCreated?(self.newCaregiver) + case .none: + break + } + + self.action = nil + } + } + + // MARK: Private + + private enum ActionType { + case cancel + case created + } + + @StateObject private var viewModel = CreateCaregiverViewModel() + + @State private var newCaregiver = Caregiver() + @State private var isAvatarPickerPresented: Bool = false + @State private var action: ActionType? + @State private var cancellables = Set() + + private var avatarPickerButton: some View { + Button { + self.isAvatarPickerPresented = true + } label: { + VStack(alignment: .center, spacing: 15) { + AvatarPicker.ButtonLabel(image: self.newCaregiver.avatar) + Text(l10n.CaregiverCreation.avatarChoiceButton) + .font(.headline) + } + } + .sheet(isPresented: self.$isAvatarPickerPresented) { + NavigationStack { + AvatarPicker(selectedAvatar: self.newCaregiver.avatar, + onValidate: { avatar in + self.newCaregiver.avatar = avatar + }) + .navigationBarTitleDisplayMode(.inline) + } + } + .frame(maxWidth: .infinity, alignment: .center) + } +} + +// MARK: - l10n.CaregiverCreation + +// swiftlint:disable line_length + +extension l10n { + enum CaregiverCreation { + static let title = LocalizedString("lekaapp.caregiver_creation.title", value: "Create a caregiver profile", comment: "Caregiver creation title") + + static let avatarChoiceButton = LocalizedString("lekaapp.caregiver_creation.avatar_choice_button", value: "Choose an avatar", comment: "Caregiver creation avatar choice button label") + + static let caregiverFirstNameLabel = LocalizedString("lekaapp.caregiver_creation.caregiver_first_name_label", value: "First name", comment: "Caregiver creation caregiver first name textfield label") + + static let caregiverLastNameLabel = LocalizedString("lekaapp.caregiver_creation.caregiver_last_name_label", value: "Last name", comment: "Caregiver creation caregiver last name textfield label") + + static let professionLabel = LocalizedString("lekaapp.caregiver_creation.profession_label", value: "Profession(s)", comment: "Caregiver creation profession label above profession selection button") + + static let professionAddButton = LocalizedString("lekaapp.caregiver_creation.profession_add_button", value: "Add", comment: "Caregiver creation profession add button label") + + static let registerProfilButton = LocalizedString("lekaapp.caregiver_creation.register_profil_button", value: "Register profile", comment: "Caregiver creation register profil button label") + } +} + +// swiftlint:enable line_length + +#Preview { + Text("Preview") + .sheet(isPresented: .constant(true)) { + NavigationStack { + CreateCaregiverView(onCancel: { print("Creation canceled") }, + onCreated: { print("Caregiver \($0.firstName) created") }) + } + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/EditCaregiverView+AccentColorRow.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/EditCaregiverView+AccentColorRow.swift new file mode 100644 index 0000000000..fd8713dcbf --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/EditCaregiverView+AccentColorRow.swift @@ -0,0 +1,70 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import SwiftUI + +// swiftlint:disable nesting + +// MARK: - EditCaregiverView.AccentColorRow + +extension EditCaregiverView { + struct AccentColorRow: View { + // MARK: Internal + + @Binding var caregiver: Caregiver + + var body: some View { + HStack { + Text(l10n.EditCaregiverView.AccentColorRow.title) + + Spacer() + + ForEach(ColorTheme.allCases, id: \.self) { color in + ColorCircleView(color: color.color, isSelected: self.selectedColor == color.color) + .onTapGesture { + self.styleManager.accentColor = color.color + self.caregiver.colorTheme = color + } + } + } + } + + // MARK: Private + + // MARK: - ColorCircleView + + private struct ColorCircleView: View { + let color: Color + let isSelected: Bool + + var body: some View { + VStack { + Circle() + .fill(self.color) + .frame(width: 25) + .overlay(Circle().fill(self.isSelected ? .white : .clear).frame(width: 8)) + .overlay(Circle().stroke(.gray, lineWidth: 0.5)) + } + .animation(.easeIn, value: self.isSelected) + } + } + + @ObservedObject private var styleManager: StyleManager = .shared + + private var selectedColor: Color { + self.styleManager.accentColor ?? .clear + } + } +} + +// swiftlint:enable nesting + +#Preview { + Form { + EditCaregiverView.AccentColorRow(caregiver: .constant(Caregiver())) + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/EditCaregiverView+AppearanceRow.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/EditCaregiverView+AppearanceRow.swift new file mode 100644 index 0000000000..d16b9f8730 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/EditCaregiverView+AppearanceRow.swift @@ -0,0 +1,44 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - EditCaregiverView.AppearanceRow + +extension EditCaregiverView { + struct AppearanceRow: View { + // MARK: Internal + + @Binding var caregiver: Caregiver + + var body: some View { + HStack(spacing: 10) { + Text(l10n.EditCaregiverView.AppearanceRow.title) + + Spacer() + + Toggle("", isOn: Binding( + get: { self.styleManager.colorScheme == .dark }, + set: { + self.styleManager.colorScheme = $0 ? .dark : .light + self.caregiver.colorScheme = $0 ? .dark : .light + } + )) + } + } + + // MARK: Private + + @ObservedObject private var styleManager: StyleManager = .shared + } +} + +#Preview { + Form { + EditCaregiverView.AppearanceRow(caregiver: .constant(Caregiver())) + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/EditCaregiverView+l10n.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/EditCaregiverView+l10n.swift new file mode 100644 index 0000000000..84030ed5f4 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/EditCaregiverView+l10n.swift @@ -0,0 +1,27 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +// swiftlint:disable line_length nesting + +extension l10n { + enum EditCaregiverView { + enum AppearanceRow { + static let title = LocalizedString("edit_caregiver_view.appearance_section.appearance_row.title", value: "Dark Mode", comment: "Appearance Row title") + } + + enum AccentColorRow { + static let title = LocalizedString("edit_caregiver_view.appearance_section.accent_color_row.title", value: "Color Theme", comment: "AccentColor Row title") + } + + static let navigationTitle = LocalizedString("edit_caregiver_view.navigation_title", value: "Edit my profile", comment: "The navigation title of Edit Caregiver View") + + static let saveButtonLabel = LocalizedString("edit_caregiver_view.save_button_label", value: "Save", comment: "Save button label of Edit Caregiver View") + + static let closeButtonLabel = LocalizedString("edit_caregiver_view.close_button_label", value: "Close", comment: "Close button label of Edit Caregiver View") + } +} + +// swiftlint:enable line_length nesting diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/EditCaregiverView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/EditCaregiverView.swift new file mode 100644 index 0000000000..3384b9a9ee --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/EditCaregiverView.swift @@ -0,0 +1,135 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - EditCaregiverView + +struct EditCaregiverView: View { + // MARK: Lifecycle + + init(caregiver: Caregiver) { + self._viewModel = StateObject(wrappedValue: EditCaregiverViewViewModel(caregiver: caregiver)) + } + + // MARK: Internal + + var body: some View { + VStack(spacing: 40) { + Form { + Section { + self.avatarPickerButton + .buttonStyle(.borderless) + .listRowBackground(Color.clear) + } + + Section { + LabeledContent(String(l10n.CaregiverCreation.caregiverFirstNameLabel.characters)) { + TextField("", text: self.$viewModel.caregiver.firstName) + .textContentType(.givenName) + .textInputAutocapitalization(.words) + .autocorrectionDisabled() + .multilineTextAlignment(.trailing) + .foregroundStyle(Color.secondary) + } + LabeledContent(String(l10n.CaregiverCreation.caregiverLastNameLabel.characters)) { + TextField("", text: self.$viewModel.caregiver.lastName) + .textContentType(.familyName) + .textInputAutocapitalization(.words) + .autocorrectionDisabled() + .multilineTextAlignment(.trailing) + .foregroundStyle(Color.secondary) + } + } + + Section { + ProfessionListView(caregiver: self.$viewModel.caregiver) + .navigationBarTitleDisplayMode(.inline) + } + + Section { + AppearanceRow(caregiver: self.$viewModel.caregiver) + AccentColorRow(caregiver: self.$viewModel.caregiver) + } + } + } + .navigationTitle(String(l10n.EditCaregiverView.navigationTitle.characters)) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(String(l10n.EditCaregiverView.closeButtonLabel.characters)) { + self.styleManager.colorScheme = self.caregiverManagerViewModel.currentCaregiver!.colorScheme + self.styleManager.accentColor = self.caregiverManagerViewModel.currentCaregiver!.colorTheme.color + self.dismiss() + } + } + ToolbarItem(placement: .topBarTrailing) { + Button(String(l10n.EditCaregiverView.saveButtonLabel.characters)) { + self.caregiverManager.updateCaregiver(caregiver: &self.viewModel.caregiver) + self.dismiss() + } + } + } + .preferredColorScheme(self.styleManager.colorScheme) + } + + // MARK: Private + + @Environment(\.dismiss) private var dismiss + + private var caregiverManager: CaregiverManager = .shared + + @StateObject private var viewModel: EditCaregiverViewViewModel + @StateObject private var caregiverManagerViewModel = CaregiverManagerViewModel() + + @ObservedObject private var styleManager: StyleManager = .shared + + private var avatarPickerButton: some View { + Button { + self.viewModel.isAvatarPickerPresented = true + } label: { + VStack(alignment: .center, spacing: 15) { + AvatarPicker.ButtonLabel(image: self.viewModel.caregiver.avatar) + Text(l10n.CaregiverCreation.avatarChoiceButton) + .font(.headline) + } + } + .sheet(isPresented: self.$viewModel.isAvatarPickerPresented) { + NavigationStack { + AvatarPicker(selectedAvatar: self.viewModel.caregiver.avatar, + onValidate: { avatar in + self.viewModel.caregiver.avatar = avatar + }) + .navigationBarTitleDisplayMode(.inline) + } + } + .frame(maxWidth: .infinity, alignment: .center) + } +} + +// MARK: - EditCaregiverViewViewModel + +class EditCaregiverViewViewModel: ObservableObject { + // MARK: Lifecycle + + init(caregiver: Caregiver) { + self.caregiver = caregiver + } + + // MARK: Internal + + @Published var caregiver: Caregiver + @Published var isAvatarPickerPresented: Bool = false +} + +#Preview { + Text("Preview") + .sheet(isPresented: .constant(true)) { + NavigationStack { + EditCaregiverView(caregiver: Caregiver()) + } + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/ProfessionListView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/ProfessionListView.swift new file mode 100644 index 0000000000..e6dd6816b7 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/Users/Caregiver/ProfessionListView.swift @@ -0,0 +1,75 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import Fit +import LocalizationKit +import SwiftUI + +struct ProfessionListView: View { + // MARK: Lifecycle + + init(caregiver: Binding) { + self._caregiver = caregiver + } + + // MARK: Internal + + var body: some View { + VStack(alignment: .leading) { + LabeledContent(String(l10n.CaregiverCreation.professionLabel.characters)) { + Button { + self.isProfessionPickerPresented = true + } label: { + Image(systemName: "plus") + } + .sheet(isPresented: self.$isProfessionPickerPresented) { + NavigationStack { + ProfessionPicker(selectedProfessionsIDs: self.caregiver.professions, + onValidate: { professions in + self.caregiver.professions = professions + }) + } + } + } + + if !self.caregiver.professions.isEmpty { + Fit { + ForEach(self.caregiver.professions, id: \.self) { id in + let profession = Professions.profession(for: id)! + + TagView(title: profession.name, systemImage: "multiply.square.fill") { + self.caregiver.professions.removeAll(where: { id == $0 }) + } + } + } + } + } + } + + // MARK: Private + + @State private var isProfessionPickerPresented: Bool = false + + @Binding private var caregiver: Caregiver +} + +#Preview { + @State var caregiver = Caregiver(professions: [ + Professions.list[0].id, + Professions.list[1].id, + Professions.list[3].id, + + ]) + + return Text("Preview") + .sheet(isPresented: .constant(true)) { + NavigationStack { + List { + ProfessionListView(caregiver: $caregiver) + } + } + } +} diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/WelcomeView+l10n.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/WelcomeView+l10n.swift new file mode 100644 index 0000000000..d6c2f44d08 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/WelcomeView+l10n.swift @@ -0,0 +1,19 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +// swiftlint:disable nesting + +extension l10n { + enum WelcomeView { + static let createAccountButton = LocalizedString("lekaapp.welcome_view.create_account_button", value: "Create account", comment: "Create account button on WelcomeView") + + static let loginButton = LocalizedString("lekaapp.welcome_view.login_button", value: "Login", comment: "Login button on WelcomeView") + + static let skipStepButton = LocalizedString("lekaapp.welcome_view.skip_step_button", value: "Skip this step", comment: "Skip step button on WelcomeView") + } +} + +// swiftlint:enable nesting diff --git a/Apps/LekaApp/Sources/_NEWCodeBase/Views/WelcomeView.swift b/Apps/LekaApp/Sources/_NEWCodeBase/Views/WelcomeView.swift new file mode 100644 index 0000000000..2dad44f064 --- /dev/null +++ b/Apps/LekaApp/Sources/_NEWCodeBase/Views/WelcomeView.swift @@ -0,0 +1,58 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - WelcomeView + +struct WelcomeView: View { + // MARK: Internal + + @Environment(\.dismiss) var dismiss + + var body: some View { + VStack(spacing: 30) { + LekaLogo(height: 90) + + NavigationLink(String(l10n.WelcomeView.createAccountButton.characters)) { + AccountCreationView() + } + .buttonStyle(.borderedProminent) + + NavigationLink(String(l10n.WelcomeView.loginButton.characters)) { + ConnectionView() + } + .buttonStyle(.bordered) + } + .navigationDestination(isPresented: self.$navigation.navigateToAccountCreationProcess) { + AccountCreationProcess.CarouselView() + .navigationBarBackButtonHidden() + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(String(l10n.WelcomeView.skipStepButton.characters)) { + self.dismiss() + } + } + } + .onAppear { + self.authManagerViewModel.userAction = .none + } + } + + // MARK: Private + + @ObservedObject private var navigation: Navigation = .shared + @ObservedObject private var authManagerViewModel = AuthManagerViewModel.shared + @StateObject private var caregiverManagerViewModel = CaregiverManagerViewModel() +} + +#Preview { + NavigationStack { + WelcomeView() + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/ContentView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/ContentView+DEPRECATED.swift new file mode 100644 index 0000000000..cba078804f --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/ContentView+DEPRECATED.swift @@ -0,0 +1,24 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct ContentViewDeprecated: View { + @EnvironmentObject var viewRouter: ViewRouterDeprecated + + var body: some View { + Group { + switch self.viewRouter.currentPage { + case .welcome: + WelcomeViewDeprecated() + .transition(.opacity) + case .home: + HomeViewDeprecated() + .transition(.opacity) + } + } + .animation(.default, value: self.viewRouter.currentPage) + .preferredColorScheme(.light) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Data/AvatarsData+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Data/AvatarsData+DEPRECATED.swift new file mode 100644 index 0000000000..9d38858ba2 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Data/AvatarsData+DEPRECATED.swift @@ -0,0 +1,241 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import Foundation + +// MARK: - AvatarCategoryDeprecated + +// Avatar Picker Models +struct AvatarCategoryDeprecated: Identifiable, Hashable { + let id = UUID() + let category: String + let images: [String] +} + +// MARK: - AvatarSetsDeprecated + +// AvatarPicker Data +enum AvatarSetsDeprecated: Int, CaseIterable, Hashable { + case girls + case boys + case lekaGirls + case lekaBoys + case jobs + case weather + case sunglasses + case animals + case fruits + case vegies + + // MARK: Internal + + var id: Self { self } + + var content: AvatarCategoryDeprecated { + switch self { + case .girls: + AvatarCategoryDeprecated( + category: "Filles", + images: [ + DesignKitAsset.Avatars.avatarsGirl1a.name, + DesignKitAsset.Avatars.avatarsGirl1b.name, + DesignKitAsset.Avatars.avatarsGirl1c.name, + DesignKitAsset.Avatars.avatarsGirl1d.name, + DesignKitAsset.Avatars.avatarsGirl1e.name, + DesignKitAsset.Avatars.avatarsGirl1f.name, + DesignKitAsset.Avatars.avatarsGirl2a.name, + DesignKitAsset.Avatars.avatarsGirl2b.name, + DesignKitAsset.Avatars.avatarsGirl2c.name, + DesignKitAsset.Avatars.avatarsGirl2d.name, + DesignKitAsset.Avatars.avatarsGirl2e.name, + DesignKitAsset.Avatars.avatarsGirl2f.name, + DesignKitAsset.Avatars.avatarsGirl3a.name, + DesignKitAsset.Avatars.avatarsGirl3b.name, + DesignKitAsset.Avatars.avatarsGirl3c.name, + DesignKitAsset.Avatars.avatarsGirl3d.name, + DesignKitAsset.Avatars.avatarsGirl3e62.name, + DesignKitAsset.Avatars.avatarsGirl3e97.name, + DesignKitAsset.Avatars.avatarsGirl4a.name, + DesignKitAsset.Avatars.avatarsGirl4b.name, + DesignKitAsset.Avatars.avatarsGirl4c.name, + DesignKitAsset.Avatars.avatarsGirl4d.name, + DesignKitAsset.Avatars.avatarsGirl4e.name, + DesignKitAsset.Avatars.avatarsGirl5a.name, + DesignKitAsset.Avatars.avatarsGirl5b.name, + DesignKitAsset.Avatars.avatarsGirl5c.name, + DesignKitAsset.Avatars.avatarsGirl5d.name, + DesignKitAsset.Avatars.avatarsGirl5e.name, + ] + ) + case .boys: + AvatarCategoryDeprecated( + category: "Garçons", + images: [ + DesignKitAsset.Avatars.avatarsBoy1a.name, + DesignKitAsset.Avatars.avatarsBoy1b.name, + DesignKitAsset.Avatars.avatarsBoy1c75.name, + DesignKitAsset.Avatars.avatarsBoy1d76.name, + DesignKitAsset.Avatars.avatarsBoy1e.name, + DesignKitAsset.Avatars.avatarsBoy1f.name, + DesignKitAsset.Avatars.avatarsBoy1g.name, + DesignKitAsset.Avatars.avatarsBoy2a.name, + DesignKitAsset.Avatars.avatarsBoy2b.name, + DesignKitAsset.Avatars.avatarsBoy2c.name, + DesignKitAsset.Avatars.avatarsBoy2d.name, + DesignKitAsset.Avatars.avatarsBoy2e82.name, + DesignKitAsset.Avatars.avatarsBoy2e115.name, + DesignKitAsset.Avatars.avatarsBoy2f.name, + DesignKitAsset.Avatars.avatarsBoy2g.name, + DesignKitAsset.Avatars.avatarsBoy3a.name, + DesignKitAsset.Avatars.avatarsBoy3b.name, + DesignKitAsset.Avatars.avatarsBoy3c.name, + DesignKitAsset.Avatars.avatarsBoy3d.name, + DesignKitAsset.Avatars.avatarsBoy3e.name, + DesignKitAsset.Avatars.avatarsBoy3f.name, + DesignKitAsset.Avatars.avatarsBoy3g.name, + DesignKitAsset.Avatars.avatarsBoy4a.name, + DesignKitAsset.Avatars.avatarsBoy4b.name, + DesignKitAsset.Avatars.avatarsBoy4c.name, + DesignKitAsset.Avatars.avatarsBoy4d.name, + DesignKitAsset.Avatars.avatarsBoy4e.name, + DesignKitAsset.Avatars.avatarsBoy4f.name, + DesignKitAsset.Avatars.avatarsBoy4g.name, + ] + ) + case .lekaGirls: + AvatarCategoryDeprecated( + category: "Filles Leka", + images: [ + DesignKitAsset.Avatars.avatarsLekaGirl1a.name, + DesignKitAsset.Avatars.avatarsLekaGirl1b.name, + DesignKitAsset.Avatars.avatarsLekaGirl1c118.name, + DesignKitAsset.Avatars.avatarsLekaGirl1c119.name, + DesignKitAsset.Avatars.avatarsLekaGirl2a.name, + DesignKitAsset.Avatars.avatarsLekaGirl2b.name, + DesignKitAsset.Avatars.avatarsLekaGirl2c.name, + DesignKitAsset.Avatars.avatarsLekaGirl2d.name, + DesignKitAsset.Avatars.avatarsLekaGirl3a.name, + DesignKitAsset.Avatars.avatarsLekaGirl3b.name, + DesignKitAsset.Avatars.avatarsLekaGirl3c.name, + DesignKitAsset.Avatars.avatarsLekaGirl3d.name, + DesignKitAsset.Avatars.avatarsLekaGirl4a.name, + DesignKitAsset.Avatars.avatarsLekaGirl4b.name, + DesignKitAsset.Avatars.avatarsLekaGirl4c.name, + DesignKitAsset.Avatars.avatarsLekaGirl4d.name, + DesignKitAsset.Avatars.avatarsLekaGirl5a.name, + DesignKitAsset.Avatars.avatarsLekaGirl5b.name, + DesignKitAsset.Avatars.avatarsLekaGirl5c.name, + DesignKitAsset.Avatars.avatarsLekaGirl5d.name, + DesignKitAsset.Avatars.avatarsLekaGirl6a.name, + DesignKitAsset.Avatars.avatarsLekaGirl6b.name, + DesignKitAsset.Avatars.avatarsLekaGirl6c.name, + DesignKitAsset.Avatars.avatarsLekaGirl6d.name, + ] + ) + case .lekaBoys: + AvatarCategoryDeprecated( + category: "Garçons Leka", + images: [ + DesignKitAsset.Avatars.avatarsLekaBoy1a.name, + DesignKitAsset.Avatars.avatarsLekaBoy1b.name, + DesignKitAsset.Avatars.avatarsBoy1c138.name, + DesignKitAsset.Avatars.avatarsBoy1d144.name, + DesignKitAsset.Avatars.avatarsLekaBoy2a.name, + DesignKitAsset.Avatars.avatarsLekaBoy2b.name, + DesignKitAsset.Avatars.avatarsLekaBoy2c.name, + DesignKitAsset.Avatars.avatarsLekaBoy2d.name, + ] + ) + case .jobs: + AvatarCategoryDeprecated( + category: "Métiers Leka", + images: [ + DesignKitAsset.Avatars.avatarsLekaCook.name, + DesignKitAsset.Avatars.avatarsLekaAstronaut.name, + DesignKitAsset.Avatars.avatarsLekaDoctor.name, + DesignKitAsset.Avatars.avatarsLekaExplorer.name, + DesignKitAsset.Avatars.avatarsLekaMarine.name, + DesignKitAsset.Avatars.avatarsLekaPirate.name, + ] + ) + case .weather: + AvatarCategoryDeprecated( + category: "Météo Leka", + images: [ + DesignKitAsset.Avatars.avatarsLekaCloud.name, + DesignKitAsset.Avatars.avatarsLekaFlake.name, + DesignKitAsset.Avatars.avatarsLekaMoon.name, + DesignKitAsset.Avatars.avatarsLekaStar.name, + DesignKitAsset.Avatars.avatarsSun.name, + ] + ) + case .sunglasses: + AvatarCategoryDeprecated( + category: "Lunettes de soleil", + images: [ + DesignKitAsset.Avatars.avatarsLekaSunglassesBlue.name, + DesignKitAsset.Avatars.avatarsLekaSunglassesGreen.name, + DesignKitAsset.Avatars.avatarsLekaSunglassesYellow.name, + DesignKitAsset.Avatars.avatarsLekaSunglassesPink.name, + ] + ) + case .animals: + AvatarCategoryDeprecated( + category: "Animaux", + images: [ + DesignKitAsset.Avatars.avatarsPictogramsAnimalsFarmBirdYellow0071.name, + DesignKitAsset.Avatars.avatarsPictogramsAnimalsFarmHorseBrown006A.name, + DesignKitAsset.Avatars.avatarsPictogramsAnimalsFarmRoosterWhite006B.name, + DesignKitAsset.Avatars.avatarsPictogramsAnimalsForestBearBrown005E.name, + DesignKitAsset.Avatars.avatarsPictogramsAnimalsForestFoxOrange0064.name, + DesignKitAsset.Avatars.avatarsPictogramsAnimalsForestHedgehogBrown0062.name, + DesignKitAsset.Avatars.avatarsPictogramsAnimalsForestRabbitGray0061.name, + DesignKitAsset.Avatars.avatarsPictogramsAnimalsForestSquirrelOrange005C.name, + DesignKitAsset.Avatars.avatarsPictogramsAnimalsPetsFishBlue0055.name, + DesignKitAsset.Avatars.avatarsPictogramsAnimalsPetsTurtleGreen0056.name, + DesignKitAsset.Avatars.avatarsPictogramsAnimalsSavannaElephantGray0085.name, + DesignKitAsset.Avatars.avatarsPictogramsAnimalsSavannaGiraffeYellow0081.name, + DesignKitAsset.Avatars.avatarsPictogramsAnimalsSavannaKangarooBrown0078.name, + DesignKitAsset.Avatars.avatarsPictogramsAnimalsSavannaKoalaGray0077.name, + DesignKitAsset.Avatars.avatarsPictogramsAnimalsSavannaLionBrown0082.name, + DesignKitAsset.Avatars.avatarsPictogramsAnimalsSeaCrabRed003E.name, + DesignKitAsset.Avatars.avatarsPictogramsAnimalsSeaTurtleGreen0041.name, + ] + ) + case .fruits: + AvatarCategoryDeprecated( + category: "Fruits", + images: [ + DesignKitAsset.Avatars.avatarsPictogramsFoodsFruitsAppleGreen0100.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsFruitsAppleRed0101.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsFruitsBananaYellow00FB.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsFruitsCherryRed00FF.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsFruitsGrapePurple00FE.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsFruitsKiwiGreen00F8.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsFruitsLemonYellow00F7.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsFruitsPearYellow00FC.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsFruitsPineappleOrange00F9.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsFruitsStrawberryRed00FD.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsFruitsWatermelonRed00FA.name, + ] + ) + case .vegies: + AvatarCategoryDeprecated( + category: "Légumes", + images: [ + DesignKitAsset.Avatars.avatarsPictogramsFoodsVegetablesAvocadoGreen00E1.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsVegetablesBroccoliGreen00E5.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsVegetablesCarrotOrange00E6.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsVegetablesCornYellow00E3.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsVegetablesEggplantPurple00E4.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsVegetablesOnionYellow00E8.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsVegetablesPotatoYellow100E9.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsVegetablesSaladGreen100EA.name, + DesignKitAsset.Avatars.avatarsPictogramsFoodsVegetablesTomatoRed00E2.name, + ] + ) + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Data/DiscoveryCompany.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Data/DiscoveryCompany.swift new file mode 100644 index 0000000000..afa688b986 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Data/DiscoveryCompany.swift @@ -0,0 +1,135 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +class DiscoveryCompany { + static var discoveryTeachers: [TeacherDeprecated] = + [ + TeacherDeprecated( + name: "Aurore", + avatar: DesignKitAsset.Avatars.avatarsLekaCook.name, + jobs: ["Psychomotricien(ne)"] + ), + TeacherDeprecated( + name: "Jean-Louis", + avatar: DesignKitAsset.Avatars.accompanyingBlue.name, + jobs: ["Psychomotricien(ne)"] + ), + TeacherDeprecated( + name: "Pauline", + avatar: DesignKitAsset.Avatars.avatarsLekaExplorer.name, + jobs: ["Ergothérapeute"] + ), + TeacherDeprecated( + name: "Jean-Pierre", + avatar: DesignKitAsset.Avatars.avatarsBoy3c.name, + jobs: ["Pédopsychiatre"] + ), + TeacherDeprecated( + name: "Anne", + avatar: DesignKitAsset.Avatars.avatarsPictogramsFoodsFruitsPineappleOrange00F9.name, + jobs: ["Accompagnant(e) des élèves en situation de handicap"] + ), + ] + + static var discoveryUsers: [UserDeprecated] = + [ + UserDeprecated( + name: "Alice", + avatar: DesignKitAsset.Avatars.avatarsGirl1a.name, + reinforcer: 3 + ), + UserDeprecated( + name: "Olivia", + avatar: DesignKitAsset.Avatars.avatarsLekaSunglassesBlue.name, + reinforcer: 5 + ), + UserDeprecated( + name: "Alexandre", + avatar: DesignKitAsset.Avatars.avatarsPictogramsAnimalsForestHedgehogBrown0062.name, + reinforcer: 1 + ), + UserDeprecated( + name: "Érica", + avatar: DesignKitAsset.Avatars.avatarsLekaMoon.name, + reinforcer: 4 + ), + UserDeprecated( + name: "Elessa", + avatar: DesignKitAsset.Avatars.avatarsPictogramsAnimalsFarmBirdYellow0071.name, + reinforcer: 1 + ), + UserDeprecated( + name: "Lucas", + avatar: DesignKitAsset.Avatars.avatarsBoy1d144.name, + reinforcer: 2 + ), + UserDeprecated( + name: "Sébastien", + avatar: DesignKitAsset.Avatars.avatarsPictogramsAnimalsForestSquirrelOrange005C.name, + reinforcer: 1 + ), + UserDeprecated( + name: "Maximilien", + avatar: DesignKitAsset.Avatars.avatarsPictogramsFoodsFruitsCherryRed00FF.name, + reinforcer: 4 + ), + UserDeprecated( + name: "Luc", + avatar: DesignKitAsset.Avatars.avatarsPictogramsAnimalsForestFoxOrange0064.name, + reinforcer: 1 + ), + UserDeprecated( + name: "Élisabeth", + avatar: DesignKitAsset.Avatars.avatarsSun.name, + reinforcer: 5 + ), + UserDeprecated( + name: "Ariane", + avatar: DesignKitAsset.Avatars.avatarsPictogramsAnimalsSavannaGiraffeYellow0081.name, + reinforcer: 1 + ), + UserDeprecated( + name: "Stéphane", + avatar: DesignKitAsset.Avatars.avatarsBoy4c.name, + reinforcer: 3 + ), + UserDeprecated( + name: "Lila", + avatar: DesignKitAsset.Avatars.avatarsPictogramsFoodsVegetablesCarrotOrange00E6.name, + reinforcer: 2 + ), + UserDeprecated( + name: "Pierre", + avatar: DesignKitAsset.Avatars.avatarsPictogramsAnimalsSavannaLionBrown0082.name, + reinforcer: 1 + ), + UserDeprecated( + name: "Baptiste", + avatar: DesignKitAsset.Avatars.avatarsBoy2d.name, + reinforcer: 5 + ), + UserDeprecated( + name: "Éloïse", + avatar: DesignKitAsset.Avatars.avatarsGirl2d.name, + reinforcer: 4 + ), + UserDeprecated( + name: "Clément", + avatar: DesignKitAsset.Avatars.avatarsLekaMoon.name, + reinforcer: 2 + ), + UserDeprecated( + name: "Simon", + avatar: DesignKitAsset.Avatars.avatarsLekaMarine.name, + reinforcer: 3 + ), + ] + + let discoveryCompany = CompanyDeprecated( + mail: "discovery@leka.io", password: "Password1234", teachers: discoveryTeachers, users: discoveryUsers + ) +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Data/Launch Screen.storyboard b/Apps/LekaApp/Sources/_OLDCodeBase/Data/Launch Screen.storyboard new file mode 100644 index 0000000000..d46b6f2f57 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Data/Launch Screen.storyboard @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Data/LekaCompany.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Data/LekaCompany.swift new file mode 100644 index 0000000000..2f63974f2a --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Data/LekaCompany.swift @@ -0,0 +1,108 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import Foundation + +class LekaCompany { + // This will only be used for tests + maybe congresses?? + let lekaCompany = CompanyDeprecated( + mail: "test@leka.io", + password: "lekaleka", + teachers: [ + TeacherDeprecated( + name: "Ladislas", + avatar: DesignKitAsset.Avatars.avatarsPictogramsFoodsFruitsBananaYellow00FB.name, + jobs: ["CEO"] + ), + TeacherDeprecated( + name: "Hortense", + avatar: DesignKitAsset.Avatars.avatarsLekaExplorer.name, + jobs: ["Designer"] + ), + TeacherDeprecated( + name: "Lucie", + avatar: DesignKitAsset.Avatars.avatarsLekaGirl6a.name, + jobs: ["COO"] + ), + TeacherDeprecated( + name: "Mathieu", + avatar: DesignKitAsset.Avatars.avatarsPictogramsAnimalsSeaCrabRed003E.name, + jobs: ["Developer"] + ), + TeacherDeprecated( + name: "Jean-Christophe B.", + avatar: DesignKitAsset.Avatars.avatarsLekaMoon.name, + jobs: ["Pédopsychiatre"] + ), + ], + users: [ + UserDeprecated( + name: "Alice", + avatar: DesignKitAsset.Avatars.avatarsLekaSunglassesBlue.name, + reinforcer: 3 + ), + UserDeprecated( + name: "Olivia", + avatar: DesignKitAsset.Avatars.avatarsLekaStar.name, + reinforcer: 5 + ), + UserDeprecated( + name: "Elessa", + avatar: DesignKitAsset.Avatars.avatarsGirl3e62.name, + reinforcer: 1 + ), + UserDeprecated( + name: "Lucas", + avatar: DesignKitAsset.Avatars.avatarsPictogramsAnimalsFarmRoosterWhite006B.name, + reinforcer: 2 + ), + UserDeprecated( + name: "Maximilien", + avatar: DesignKitAsset.Avatars.avatarsPictogramsFoodsVegetablesCornYellow00E3.name, + reinforcer: 4 + ), + UserDeprecated( + name: "Stéphane", + avatar: DesignKitAsset.Avatars.avatarsPictogramsAnimalsSeaTurtleGreen0041.name, + reinforcer: 3 + ), + UserDeprecated( + name: "Lila", + avatar: DesignKitAsset.Avatars.avatarsPictogramsAnimalsSeaCrabRed003E.name, + reinforcer: 2 + ), + UserDeprecated( + name: "Pierre", + avatar: DesignKitAsset.Avatars.avatarsBoy2d.name, + reinforcer: 1 + ), + UserDeprecated( + name: "Baptiste", + avatar: DesignKitAsset.Avatars.avatarsPictogramsAnimalsForestHedgehogBrown0062.name, + reinforcer: 5 + ), + UserDeprecated( + name: "Éloïse", + avatar: DesignKitAsset.Avatars.avatarsSun.name, + reinforcer: 4 + ), + UserDeprecated( + name: "Clément", + avatar: DesignKitAsset.Avatars.avatarsLekaBoy2d.name, + reinforcer: 2 + ), + UserDeprecated( + name: "Simon", + avatar: DesignKitAsset.Avatars.avatarsLekaMarine.name, + reinforcer: 3 + ), + UserDeprecated( + name: "Jean-Pierre Marie", + avatar: DesignKitAsset.Avatars.avatarsPictogramsFoodsFruitsStrawberryRed00FD.name, + reinforcer: 4 + ), + ] + ) +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Data/TilesContent.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Data/TilesContent.swift new file mode 100644 index 0000000000..11271d43ca --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Data/TilesContent.swift @@ -0,0 +1,108 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import Foundation +import SwiftUI + +// MARK: - TileContent + +// swiftlint:disable line_length + +struct TileContent { + var image: String? + var title: String? + var subtitle: String? + var message: String? + var callToActionLabel: String? + var pictoCTA: String? +} + +// MARK: - TileData + +enum TileData: Int, CaseIterable, Hashable { + case discovery + case curriculums + case activities + case commands + case signupBravo + case signupStep1 + case signupStep2 + case signupFinalStep + + // MARK: Internal + + var id: Self { self } + + var content: TileContent { + switch self { + // DiscoveryMode Orange Tile + case .discovery: + TileContent( + image: "exclamationmark.triangle", + title: "Le mode découverte", + subtitle: "Vous utilisez actuellement votre application en mode découverte !", + message: "Vous ne pouvez pas créer de profils et aucune donnée ne sera enregistrée.", + callToActionLabel: "Se connecter ou Créer un compte" + ) + // Blue Information tiles + case .curriculums: + TileContent( + image: "graduationcap", + title: "Les parcours", + subtitle: "Les parcours sont des compilations d'activités dont la difficulté est évolutive.", + message: + "Les parcours ont pour objectif d'atteindre des compétences précises. Ils ont été pensé avec des professionnels du médico-social. \nVous pouvez réaliser les activités dans l'ordre ou sauter des niveaux." + ) + case .activities: + TileContent( + image: "dice", + title: "Les activités", + subtitle: "Les activités vous permettent de travailler des compétences variées !", + message: + "Vous trouverez au sein de votre application diverses activités variées. Elles peuvent intéresser les différents métiers du médico-social afin de faire progresser les utilisateurs." + ) + case .commands: + TileContent( + image: "gamecontroller", + title: "Les commandes", + subtitle: + "Les commandes vous permettent de créer des activités en utilisant Leka comme médiateur !", + message: + "Vous pouvez télécommander Leka, le faire tourner, lancer un renforçateur ou encore allumer ses leds dans la couleur souhaitée ! \nL'objectif des commandes est de vous permettre de créer votre propre activité avec l'utilisateur et d'entrer en interaction avec lui." + ) + // New company signup path + case .signupBravo: + TileContent( + image: DesignKitAsset.Images.welcome.name, + title: "Félicitations ! 🎉 \nVous venez de créer votre compte Leka !", + message: "Nous allons maintenant découvrir l'application \nensemble. Vous êtes prêt ?", + callToActionLabel: "👉 C'est parti !" + ) + case .signupStep1: + TileContent( + image: DesignKitAsset.Images.accompagnantPicto.name, + title: "ÉTAPE 1 :", + message: "Nous allons créer votre profil accompagnant.", + callToActionLabel: "Créer" + ) + case .signupStep2: + TileContent( + image: DesignKitAsset.Images.user.name, + title: "ÉTAPE 2 :", + message: + "Nous allons maintenant créer votre premier \nprofil utilisateur (le profil d'une personne que \nvous accompagnez).", + callToActionLabel: "Créer" + ) + case .signupFinalStep: + TileContent( + title: "🎉 Encore bravo ! 👏", + message: "Vous avez réalisé ces 2 étapes avec brio :", + callToActionLabel: "Découvrir le contenu !" + ) + } + } +} + +// swiftlint:enable line_length diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Extensions/Extensions+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Extensions/Extensions+DEPRECATED.swift new file mode 100644 index 0000000000..312b6b38aa --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Extensions/Extensions+DEPRECATED.swift @@ -0,0 +1,93 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AVFoundation +import DesignKit +import Foundation +import SwiftUI + +// MARK: - Shape + +// Fill & Stroke with 1 modifier +extension Shape { + func fillDeprecated( + _ fillStyle: some ShapeStyle, strokeBorder strokeStyle: some ShapeStyle, lineWidth: CGFloat = 1 + ) -> some View { + stroke(strokeStyle, lineWidth: lineWidth) + .background(self.fill(fillStyle)) + } +} + +// MARK: - String + +// Check if email format is correct +extension String { + func isValidEmailDeprecated() -> Bool { + let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let emailPred = NSPredicate(format: "SELF MATCHES %@", emailRegEx) + return emailPred.evaluate(with: self) + } +} + +// MARK: - Image + +// Used for Activities Icons && Commands/Stories buttons +extension Image { + func activityIconImageModifier(diameter: CGFloat = 132, padding: CGFloat = 0) -> some View { + ZStack { + Circle() + .fill(.white) + .shadow(color: .black.opacity(0.2), radius: 2.5, x: 0, y: 2.6) + self + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: diameter, maxHeight: diameter) + .mask(Circle()) + .padding(padding) + Circle() + .strokeBorder(DesignKitAsset.Colors.btnLightBlue.swiftUIColor, lineWidth: 4) + } + .frame(minWidth: diameter, maxWidth: diameter) + } +} + +// MARK: - dismiss Keyboard when focusState not available + +#if canImport(UIKit) + extension View { + func hideKeyboard() { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } + } +#endif + +// MARK: - Localized Custom Type for Yaml Translations + +extension LocalizedContent { + func localized() -> String { + guard let translation = (Locale.current.language.languageCode?.identifier == "fr" ? frFR : enUS) + else { + print("Nothing to display") + return "" + } + return translation + } +} + +// MARK: - ActivityViewModelDeprecated + AVSpeechSynthesizerDelegate + +extension ActivityViewModelDeprecated: AVSpeechSynthesizerDelegate { + func speechSynthesizer(_: AVSpeechSynthesizer, didFinish _: AVSpeechUtterance) { + isSpeaking = false + } +} + +// MARK: - ActivityViewModelDeprecated + AVAudioPlayerDelegate + +extension ActivityViewModelDeprecated: AVAudioPlayerDelegate { + func audioPlayerDidFinishPlaying(_: AVAudioPlayer, successfully _: Bool) { + currentMediaHasBeenPlayedOnce = true + answersAreDisabled = false + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/HomeView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/HomeView+DEPRECATED.swift new file mode 100644 index 0000000000..29dce53177 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/HomeView+DEPRECATED.swift @@ -0,0 +1,90 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import RobotKit +import SwiftUI + +// MARK: - HomeViewDeprecated + +struct HomeViewDeprecated: View { + // MARK: Internal + + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + Group { + // Educ Content + NavigationSplitView(columnVisibility: self.$navigationVM.sidebarVisibility) { + SidebarViewDeprecated() + } detail: { + NavigationStack(path: self.$navigationVM.pathsFromHome) { + self.navigationVM.allSidebarDestinationViews + .navigationBarTitleDisplayMode(.inline) + .navigationTitle(self.navigationVM.getNavTitle()) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + self.infoButton + } + } + .background(DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea()) + } + } + } + .preferredColorScheme(.light) + .sheet(isPresented: self.$navigationVM.showSettings) { + SettingsViewDeprecated() + } + .fullScreenCover(isPresented: self.$navigationVM.showProfileEditor) { + NavigationStack { + ProfileEditorView() + } + } + .fullScreenCover(isPresented: self.$navigationVM.showRobotPicker) { + RobotConnectionView() + } + .fullScreenCover(isPresented: self.$navigationVM.showActivitiesFullScreenCover) { + FullScreenCoverToGameViewDeprecated() + } + .alert("Voulez-vous quitter le mode exploratoire ?", isPresented: self.$settings.showSwitchOffExploratoryAlert) { + Button(role: .destructive) { + self.settings.exploratoryModeIsOn.toggle() + } label: { + Text("Quitter") + } + } message: { + Text(""" + Vous êtes actuellement en mode exploratoire. \ + Ce mode vous permet d'explorer les contenus \ + éducatifs sans que l'utilisation ne soit enregistrée. + """ + ) + } + } + + // MARK: Private + + private var infoButton: some View { + Button { + self.navigationVM.updateShowInfo() + } label: { + Image(systemName: "info.circle") + } + .opacity(self.navigationVM.showInfo() ? 0 : 1) + } +} + +// MARK: - HomeView_Previews + +struct HomeView_Previews: PreviewProvider { + static var previews: some View { + HomeViewDeprecated() + .environmentObject(NavigationViewModelDeprecated()) + .environmentObject(SettingsViewModelDeprecated()) + .environmentObject(UIMetrics()) + .previewInterfaceOrientation(.landscapeLeft) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/MainApp+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/MainApp+DEPRECATED.swift new file mode 100644 index 0000000000..9116ffbf09 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/MainApp+DEPRECATED.swift @@ -0,0 +1,34 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct LekaAppDeprecated: App { + @StateObject var viewRouter = ViewRouterDeprecated() + @StateObject var metrics = UIMetrics() + @StateObject var navigationVM = NavigationViewModelDeprecated() + @StateObject var company = CompanyViewModelDeprecated() + @StateObject var settings = SettingsViewModelDeprecated() + @StateObject var curriculumVM = CurriculumViewModelDeprecated() + @StateObject var activityVM = ActivityViewModelDeprecated() + @StateObject var robotVM = RobotViewModel() + + var body: some Scene { + WindowGroup { + ContentViewDeprecated() + .task { + self.curriculumVM.populateCurriculumList(category: .emotionRecognition) + self.curriculumVM.getCompleteActivityList() + } + .environmentObject(self.viewRouter) + .environmentObject(self.metrics) + .environmentObject(self.navigationVM) + .environmentObject(self.company) + .environmentObject(self.settings) + .environmentObject(self.curriculumVM) + .environmentObject(self.activityVM) + .environmentObject(self.robotVM) + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Models/ActivityModel+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Models/ActivityModel+DEPRECATED.swift new file mode 100644 index 0000000000..4044d501f2 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Models/ActivityModel+DEPRECATED.swift @@ -0,0 +1,116 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// MARK: - ActivityCellDeprecated + +struct ActivityCellDeprecated: Identifiable { + var id = UUID() + var img: String + var texts: [String] +} + +// MARK: - InstructionsDeprecated + +struct InstructionsDeprecated: Codable { + // MARK: Lifecycle + + init(instructions: LocalizedContent = LocalizedContent()) { + self.instructions = instructions + } + + // MARK: Internal + + var instructions: LocalizedContent +} + +// MARK: - ActivityDeprecated + +struct ActivityDeprecated: Codable { + // MARK: Lifecycle + + init( + id: String = "", + title: LocalizedContent = LocalizedContent(), + short: LocalizedContent = LocalizedContent(), + activityType: String? = "touch_to_select", + stepsAmount: Int = 0, + isRandom: Bool = false, + numberOfImages: Int = 0, + randomImagePosition: Bool = false, + steps: [StepDeprecated] = [] + ) { + self.id = id + self.title = title + self.short = short + self.activityType = activityType + self.stepsAmount = stepsAmount + self.isRandom = isRandom + self.numberOfImages = numberOfImages + self.randomImagePosition = randomImagePosition + self.steps = steps + } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case title + case steps + case short = "short_title" + case id = "uuid" + case activityType = "type" + case stepsAmount = "number_of_steps" + case isRandom = "random_steps" + case numberOfImages = "number_of_images_per_step" + case randomImagePosition = "random_image_position" + } + + var id: String + var title: LocalizedContent + var short: LocalizedContent + var activityType: String? + var stepsAmount: Int + var isRandom: Bool + var numberOfImages: Int + var randomImagePosition: Bool + var steps: [StepDeprecated] +} + +// MARK: - StepDeprecated + +// Step conforms to Equatable because steps are compared when randomized +struct StepDeprecated: Codable, Equatable { + // MARK: Lifecycle + + init( + instruction: LocalizedContent = LocalizedContent(), + correctAnswer: String = "", + images: [String] = [], + sound: [String]? = [] + ) { + self.instruction = instruction + self.correctAnswer = correctAnswer + self.images = images + self.sound = sound + } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case instruction + case images + case sound + case correctAnswer = "correct_answer" + } + + var instruction: LocalizedContent + var correctAnswer: String + var images: [String] + var sound: [String]? + + static func == (lhs: StepDeprecated, rhs: StepDeprecated) -> Bool { + lhs.instruction == rhs.instruction + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Models/CompanyModel+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Models/CompanyModel+DEPRECATED.swift new file mode 100644 index 0000000000..f7dfd43101 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Models/CompanyModel+DEPRECATED.swift @@ -0,0 +1,119 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// MARK: - UserTypeDeprecated + +enum UserTypeDeprecated: Int, CaseIterable { + case user + case teacher +} + +// MARK: - CompanyDeprecated + +struct CompanyDeprecated: Identifiable { + var id = UUID() + var mail: String + var password: String + var teachers: [TeacherDeprecated] = [] + var users: [UserDeprecated] = [] +} + +// MARK: - ProfileDeprecated + +// Profiles types base-protocol +protocol ProfileDeprecated: Identifiable, Hashable { + var id: UUID { get } + var type: UserTypeDeprecated { get } + var name: String { get set } + var avatar: String { get set } +} + +// MARK: - TeacherDeprecated + +struct TeacherDeprecated: ProfileDeprecated { + // conform + let id = UUID() + let type = UserTypeDeprecated.teacher + var name: String + var avatar: String + + // specific + var jobs: [String] +} + +// MARK: - UserDeprecated + +struct UserDeprecated: ProfileDeprecated { + // conform + let id = UUID() + let type = UserTypeDeprecated.user + var name: String + var avatar: String + + // specific + var reinforcer: Int +} + +// MARK: - ProfessionsDeprecated + +enum ProfessionsDeprecated: String, Identifiable, CaseIterable { + // swiftlint:disable identifier_name + case educSpe + case eje + case monit + case monitAt + case teach + case ASC + case psychoMot + case ergo + case ortho + case kine + case pedopsy + case med + case psy + case infir + case soign + case AESH + case AES + case AVDH + case auxVieScol + case pueri + case auxPueri + case ludo + + // MARK: Internal + + // swiftlint:enable identifier_name + + var id: Self { self } + + var name: String { + switch self { + case .educSpe: "Éducateur(trice) spécialisé(e)" + case .eje: "Éducateur(trice) de jeunes enfants" + case .monit: "Moniteur(trice) éducateur(trice)" + case .monitAt: "Moniteur(trice) d'atelier" + case .teach: "Enseignant(e)" + case .ASC: "Animateur(trice) socio-culturel(le)" + case .psychoMot: "Psychomotricien(ne)" + case .ergo: "Ergothérapeute" + case .ortho: "Orthophoniste" + case .kine: "Kinésithérapeute" + case .pedopsy: "Pédopsychiatre" + case .med: "Médecin" + case .psy: "Psychologue" + case .infir: "Infirmier(ière)" + case .soign: "Soignant(e)" + case .AESH: "Accompagnant(e) des élèves en situation de handicap" + case .AES: "Accompagnant(e) éducatif et social" + case .AVDH: "Assistant(e) de vie dépendance et handicap" + case .auxVieScol: "Auxiliaire de vie sociale" + case .pueri: "Puériculteur(trice)" + case .auxPueri: "Auxiliaire de puériculture" + case .ludo: "Ludothécaire" + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Models/CurriculumModel.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Models/CurriculumModel.swift new file mode 100644 index 0000000000..6db63c2c73 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Models/CurriculumModel.swift @@ -0,0 +1,69 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +// MARK: - CurriculumList + +struct CurriculumList: Codable { + // MARK: Lifecycle + + init( + sectionTitle: LocalizedContent = LocalizedContent(), + curriculums: [String] = [] + ) { + self.sectionTitle = sectionTitle + self.curriculums = curriculums + } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case curriculums + case sectionTitle = "section_title" + } + + var sectionTitle: LocalizedContent + var curriculums: [String] +} + +// MARK: - Curriculum + +struct Curriculum: Codable, Identifiable { + // MARK: Lifecycle + + init( + id: String = "", + title: LocalizedContent = LocalizedContent(), + subtitle: LocalizedContent = LocalizedContent(), + fullTitle: LocalizedContent = LocalizedContent(), + quantity: Int = 0, + activities: [String] = [] + ) { + self.id = id + self.title = title + self.subtitle = subtitle + self.fullTitle = fullTitle + self.quantity = quantity + self.activities = activities + } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case title + case subtitle + case activities + case id = "uuid" + case fullTitle = "full_title" + case quantity = "number_of_activities" + } + + var id: String + var title: LocalizedContent + var subtitle: LocalizedContent + var fullTitle: LocalizedContent + var quantity: Int + var activities: [String] +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Models/SidebarModel.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Models/SidebarModel.swift new file mode 100644 index 0000000000..de5bc5c554 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Models/SidebarModel.swift @@ -0,0 +1,44 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// MARK: - SidebarDestinations + +// Navigation Sets +enum SidebarDestinations: Int, Identifiable, CaseIterable, Hashable { + case curriculums + case activities + case commands + + // MARK: Internal + + var id: Self { self } +} + +// MARK: - PathsToGame + +enum PathsToGame: Hashable { + case robot + case user + case game +} + +// MARK: - SectionLabel + +// Sidebar UI Models +struct SectionLabel: Identifiable, Hashable { + let id = UUID() + let destination: SidebarDestinations + let icon: String + let label: String +} + +// MARK: - ListModel + +struct ListModel: Identifiable, Hashable { + let id = UUID() + let title: String + let sections: [SectionLabel] +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Models/YAMLDecoder/YAMLDecoder.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Models/YAMLDecoder/YAMLDecoder.swift new file mode 100644 index 0000000000..14fb272cc3 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Models/YAMLDecoder/YAMLDecoder.swift @@ -0,0 +1,66 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import Yams + +// MARK: - YamlFileDecodable + +protocol YamlFileDecodable { + func decodeYamlFile(withName name: String, toType: T.Type) throws -> T +} + +extension YamlFileDecodable { + func decodeYamlFile(withName name: String, toType _: T.Type) throws -> T { + guard let path = Bundle.main.path(forResource: name, ofType: "yml") else { + print(name) + throw CustomError.failedToGetFilePath + } + + let yamlString = try String(contentsOfFile: path) + let decoder = YAMLDecoder() + let decoded = try decoder.decode(T.self, from: yamlString) + return decoded + } +} + +// MARK: - CustomError + +enum CustomError: Error, CustomStringConvertible { + case failedToGetFilePath + + // MARK: Internal + + var description: String { + switch self { + case .failedToGetFilePath: "Unable to get the path to the Yaml file!" + } + } +} + +// MARK: - YamlFiles + +struct YamlFiles: RawRepresentable, Hashable { + // MARK: Lifecycle + + init?(rawValue: String) { + self.rawValue = rawValue + } + + init(_ rawValue: String) { + self.rawValue = rawValue + } + + // MARK: Internal + + var rawValue: String +} + +// MARK: - CurriculumCategories + +enum CurriculumCategories: String, CaseIterable { + case emotionRecognition = "emotion_recognition-curriculums-list" + case categorization = "categorization_curriculums-list" + case receptiveLanguage = "receptive-language_curriculums-list" +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Stores/ActivityViewModel+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Stores/ActivityViewModel+DEPRECATED.swift new file mode 100644 index 0000000000..8c320044c5 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Stores/ActivityViewModel+DEPRECATED.swift @@ -0,0 +1,299 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AVFoundation +import SwiftUI +import UIKit +import Yams + +// @MainActor +class ActivityViewModelDeprecated: NSObject, ObservableObject, YamlFileDecodable { + // MARK: - Current Activity's properties + + @Published var currentActivity = ActivityDeprecated() + @Published var selectedActivityID: UUID? // save scroll position + @Published var currentActivityTitle: String = "" + @Published var currentActivityType: String = "touch_to_select" + @Published var steps: [StepDeprecated] = [] + @Published var numberOfSteps: Int = 0 + @Published var currentStep: Int = 0 + @Published var images: [String] = [] + @Published var correctIndex: Int = 0 + + // AudioPlayer - Media related Activities + @Published var sound: String = "" + @Published var audioPlayer: AVAudioPlayer! + @Published var progress: CGFloat = 0.0 + @Published var currentMediaHasBeenPlayedOnce: Bool = false + @Published var answersAreDisabled: Bool = false + + // MARK: - Game-related animations & Interactions + + @Published var tapIsDisabled: Bool = false + @Published var pressedIndex: Int = 100 + @Published var trials: Int = 0 + @Published var markerColors: [Color] = [] + @Published var overlayOpacity: Double = 0 + @Published var percent: CGFloat = .zero + @Published var showMotivator: Bool = false + @Published var showBlurryBG: Bool = false + @Published var showEndAnimation: Bool = false + @Published var goodAnswers: Int = 0 + @Published var percentOfSuccess: Int = 0 + @Published var result: ResultType = .idle + + // MARK: - UI-Based interactions (delegate in Extensions.swift) + + // Here because GameMetrics is @EnvironmentObject + @Published var isSpeaking: Bool = false + @Published var synth = AVSpeechSynthesizer() + + func getActivity(_ title: String) -> ActivityDeprecated { + do { + return try decodeYamlFile(withName: title, toType: ActivityDeprecated.self) + } catch { + print("Activities: Failed to decode Yaml file with error:", error) + return ActivityDeprecated() + } + } + + // Temporary Instructions Source + func getInstructions() -> String { + do { + return try decodeYamlFile(withName: "instructions", toType: InstructionsDeprecated.self).instructions.localized() + } catch { + print("Instructions: Failed to decode Yaml file with error:", error) + return InstructionsDeprecated().instructions.localized() + } + } + + func speak(sentence: String) { + self.synth.delegate = self + let utterance = AVSpeechUtterance(string: sentence) + utterance.rate = 0.40 + utterance.voice = + Locale.current.language.languageCode?.identifier == "fr" + ? AVSpeechSynthesisVoice(language: "fr-FR") : AVSpeechSynthesisVoice(language: "en-US") + + self.isSpeaking = true + self.synth.speak(utterance) + } + + // AudioPlayer (delegate in Extensions.swift) + func setAudioPlayer() { + do { + let path = Bundle.main.path(forResource: self.sound, ofType: "mp3")! + self.audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path)) + } catch { + print("ERROR - mp3 file not found - \(error)") + return + } + + self.audioPlayer.prepareToPlay() + self.audioPlayer.delegate = self + + Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in + if let player = self.audioPlayer { + self.progress = CGFloat(player.currentTime / player.duration) + } + } + } + + // MARK: - GameEngine Methods + + // fetch selected activity's data + setup and randomize + func setupGame(with: ActivityDeprecated) { + self.currentActivityTitle = with.short.localized() + self.currentActivityType = with.activityType ?? "touch_to_select" + self.numberOfSteps = with.stepsAmount + self.steps = with.steps + // If current activity has to repeat steps (assuming the model is always 5 provided vs. 10 expected steps...) + if self.steps.count != 10 { + self.steps = with.steps + with.steps + } + self.randomizeSteps() + self.result = .idle + self.goodAnswers = 0 + self.currentStep = 0 + self.populateMarkerColors() + self.setupCurrentStep() + } + + // Randomize steps & prevent 2 identical steps in a row + func randomizeSteps() { + if self.currentActivity.isRandom { + self.steps.shuffle() + var store: [StepDeprecated] = [] + var buffer: [StepDeprecated] = [] + var previous = StepDeprecated() + for step in self.steps { + if step == previous { + store.append(step) + } else { + buffer.append(step) + } + previous = step + } + self.steps = store + (store.last == buffer.first ? buffer.reversed() : buffer) + } + } + + // Initialization of the progressBar with empty Markers + func populateMarkerColors() { + self.markerColors = [] + for _ in self.steps { + self.markerColors.append(Color.clear) + } + } + + // setup player and "buttons' overlay" if needed depending on activity type + func checkMediaAvailability() { + if self.currentActivityType != "touch_to_select" { + self.setAudioPlayer() + self.currentMediaHasBeenPlayedOnce = false + self.answersAreDisabled = true + } else { + // keep the white overlay hidden upon answers' buttons when no media is available + self.currentMediaHasBeenPlayedOnce = true + self.answersAreDisabled = false + } + } + + // reinitialize step properties & setup for new step + func setupCurrentStep() { + self.trials = 0 + self.percent = 0 + self.pressedIndex = 100 + self.images = self.steps[self.currentStep].images + self.sound = self.steps[self.currentStep].sound?[0] ?? "" + self.checkMediaAvailability() + // Randomize answers + if self.currentActivity.randomImagePosition { + self.images.shuffle() + } + // Pick up Correct answer + for (index, answer) in self.images.enumerated() where answer == self.steps[self.currentStep].correctAnswer { + correctIndex = index + } + } + + // Prevent multiple taps, deal with success or failure + func answerHasBeenPressed(atIndex: Int) { + self.tapIsDisabled.toggle() // true + self.trials += 1 + self.pressedIndex = atIndex + if self.pressedIndex == self.correctIndex { + self.rewardsAnimations() + } else { + self.tryStepAgain() + } + } + + // After good answer, reward and play next (if available) or show final animation screen + func rewardsAnimations() { + if self.currentStep < self.numberOfSteps - 1 { + withAnimation(.easeOut(duration: 0.8)) { + self.percent = 1.0 + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.showMotivator.toggle() // true + self.showBlurryBG.toggle() + self.runMotivatorScenario() + } + } + } else if self.currentStep == self.numberOfSteps - 1 { + withAnimation(.easeOut(duration: 0.8)) { + self.percent = 1.0 + // Final step updated here to be seen by user & included in the final percent count + self.updateMarkers(atIndex: self.currentStep) + self.calculateSuccessPercent() + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.showEndAnimation.toggle() // true + self.showBlurryBG = true + self.tapIsDisabled.toggle() // false + } + } + } + } + + // Show, Then Hide Reinforcer animation + Setup for next Step + func runMotivatorScenario() { + self.showBlurryBG = true + // Update behind-the-scene during Reinforcer animation + DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { + self.tapIsDisabled.toggle() // false + self.pressedIndex = 100 + self.updateMarkers(atIndex: self.currentStep) + self.currentStep += 1 + self.setupCurrentStep() + } + } + + // Method that runs after the Motivator Lottie animation completes + func hideMotivator() { + self.showMotivator = false + self.showBlurryBG = false + } + + // Trigger failure animation, Play again after failure(s) + func tryStepAgain() { + withAnimation { + self.overlayOpacity = 0.8 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { + withAnimation { + self.overlayOpacity = 0 + } + self.tapIsDisabled.toggle() // false + self.pressedIndex = 100 + } + } + } + + // Update 1 marker in the ProgressBar + goodAnswer's count + func setMarkerColor() -> Color { + var color: Color = .clear + if self.trials == 1 { + color = .green + self.goodAnswers += 1 + } else if self.trials == 2 { + color = .orange + } else if self.trials > 2 { + color = .red + } + return color + } + + // Update current Marker after success + func updateMarkers(atIndex: Int) { + self.markerColors[atIndex] = self.setMarkerColor() + } + + // Final % of good answers + func calculateSuccessPercent() { + self.percentOfSuccess = Int((Double(self.goodAnswers) * 100) / Double(self.numberOfSteps).rounded(.toNearestOrAwayFromZero)) + if self.percentOfSuccess == 0 { + self.result = .fail + } else if self.percentOfSuccess >= 80 { + self.result = .success + } else { + self.result = .medium + } + } + + // Transition to the beginning of Current activity for replay + func replayCurrentActivity() { + Task { + self.setupGame(with: self.currentActivity) + self.showBlurryBG = false + self.showEndAnimation = false + } + } + + // Set selected activity to empty + func resetActivity() { + // + reset those 2 properties for next round + self.showBlurryBG = false + self.showEndAnimation = false + // selectedActivity = Activity() + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Stores/CompanyViewModel+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Stores/CompanyViewModel+DEPRECATED.swift new file mode 100644 index 0000000000..3ba7e277ce --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Stores/CompanyViewModel+DEPRECATED.swift @@ -0,0 +1,274 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +class CompanyViewModelDeprecated: ObservableObject { + @Published var currentCompany = CompanyDeprecated(mail: "", password: "", teachers: [], users: []) + @Published var profilesInUse: [UserTypeDeprecated: UUID] = [.teacher: UUID(), .user: UUID()] + @Published var selectedProfiles: [UserTypeDeprecated: UUID] = [.teacher: UUID(), .user: UUID()] + + // Buffer profiles to temporarilly store changes + @Published var bufferTeacher = TeacherDeprecated(name: "", avatar: DesignKitAsset.Avatars.accompanyingWhite.name, jobs: []) + @Published var bufferUser = UserDeprecated(name: "", avatar: DesignKitAsset.Avatars.userWhite.name, reinforcer: 1) + @Published var editingProfile: Bool = false + + // MARK: - METHODS + + // Account Managment + func disconnect() { + self.currentCompany = CompanyDeprecated(mail: "", password: "", teachers: [], users: []) + self.profilesInUse = [.teacher: UUID(), .user: UUID()] + self.selectedProfiles = [.teacher: UUID(), .user: UUID()] + self.resetBufferProfile(.teacher) + self.resetBufferProfile(.user) + } + + func assignCurrentProfiles() { + self.profilesInUse = self.selectedProfiles + } + + func preselectCurrentProfiles() { + self.selectedProfiles = self.profilesInUse + } + + // Sort profiles (alpabetically + current first) before displaying them in a ProfileSet (Selector || Editor) + func sortProfiles(_ type: UserTypeDeprecated) { + switch type { + case .teacher: + self.currentCompany.teachers.sort { $0.name < $1.name } + if let i = currentCompany.teachers.firstIndex(where: { $0.id == profilesInUse[.teacher] }) { + self.currentCompany.teachers.move(fromOffsets: [i], toOffset: 0) + } + case .user: + self.currentCompany.users.sort { $0.name < $1.name } + if let i = currentCompany.users.firstIndex(where: { $0.id == profilesInUse[.user] }) { + self.currentCompany.users.move(fromOffsets: [i], toOffset: 0) + } + } + self.preselectCurrentProfiles() + } + + func getSelectedProfileAvatar(_ type: UserTypeDeprecated) -> String { + switch type { + case .teacher: self.bufferTeacher.avatar + case .user: self.bufferUser.avatar + } + } + + func getProfileDataFor(_ type: UserTypeDeprecated, id: UUID) -> [String] { + switch type { + case .teacher: + guard let i = currentCompany.teachers.firstIndex(where: { $0.id == id }) else { + return [ + DesignKitAsset.Avatars.accompanyingBlue.name, + "Accompagnant", + ] + } + return [self.currentCompany.teachers[i].avatar, self.currentCompany.teachers[i].name] + case .user: + guard let i = currentCompany.users.firstIndex(where: { $0.id == id }) else { + return [ + !self.profileIsAssigned(.user) + ? DesignKitAsset.Avatars.questionMark.name : DesignKitAsset.Avatars.userBlue.name, + "Utilisateur", + ] + } + return [self.currentCompany.users[i].avatar, self.currentCompany.users[i].name] + } + } + + func getCurrentUserReinforcer() -> Int { + guard let i = currentCompany.users.firstIndex(where: { $0.id == profilesInUse[.user] }) else { + return 1 + } + return self.currentCompany.users[i].reinforcer + } + + func getReinforcerFor(index: Int) -> UIImage { + switch index { + case 2: DesignKitAsset.Reinforcers.spinBlinkBlueViolet.image + case 3: DesignKitAsset.Reinforcers.fire.image + case 4: DesignKitAsset.Reinforcers.sprinkles.image + case 5: DesignKitAsset.Reinforcers.rainbow.image + default: DesignKitAsset.Reinforcers.spinBlinkGreenOff.image + } + } + + func getAllAvatarsOf(_ type: UserTypeDeprecated) -> [[UUID: String]] { + switch type { + case .teacher: self.currentCompany.teachers.map { [$0.id: $0.avatar] } + case .user: self.currentCompany.users.map { [$0.id: $0.avatar] } + } + } + + func getAllProfilesIDFor(_ type: UserTypeDeprecated) -> [UUID] { + switch type { + case .teacher: self.currentCompany.teachers.map(\.id) + case .user: self.currentCompany.users.map(\.id) + } + } + + func resetBufferProfile(_ type: UserTypeDeprecated) { + switch type { + case .teacher: + self.bufferTeacher = TeacherDeprecated( + name: "", + avatar: DesignKitAsset.Avatars.accompanyingWhite.name, + jobs: [] + ) + case .user: + self.bufferUser = UserDeprecated( + name: "", + avatar: DesignKitAsset.Avatars.userWhite.name, + reinforcer: 1 + ) + } + } + + func emptyProfilesSelection() { + self.selectedProfiles[.user] = UUID() + self.selectedProfiles[.teacher] = UUID() + } + + func profileIsSelected(_ type: UserTypeDeprecated) -> Bool { + switch type { + case .teacher: + if let id = selectedProfiles[.teacher] { + return self.currentCompany.teachers.map(\.id).contains(id) + } + case .user: + if let id = selectedProfiles[.user] { + return self.currentCompany.users.map(\.id).contains(id) + } + } + + return false + } + + func profileIsCurrent(_ type: UserTypeDeprecated, id: UUID) -> Bool { + switch type { + case .teacher: self.profilesInUse[.teacher] == id + case .user: self.profilesInUse[.user] == id + } + } + + func profileIsAssigned(_ type: UserTypeDeprecated) -> Bool { + switch type { + case .teacher: + if let id = selectedProfiles[.teacher] { + return self.currentCompany.teachers.map(\.id).contains(id) + } + case .user: + if let id = selectedProfiles[.user] { + return self.currentCompany.users.map(\.id).contains(id) + } + } + + return false + } + + func selectionSetIsCorrect() -> Bool { + if let teacher = selectedProfiles[.teacher], let user = selectedProfiles[.user] { + return self.currentCompany.teachers.map(\.id).contains(teacher) + && self.currentCompany.users.map(\.id).contains(user) + } + + return false + } + + func editProfile(_ type: UserTypeDeprecated) { + switch type { + case .teacher: + if let i = currentCompany.teachers.firstIndex(where: { $0.id == selectedProfiles[.teacher] }) { + self.bufferTeacher = self.currentCompany.teachers[i] + } + case .user: + if let i = currentCompany.users.firstIndex(where: { $0.id == selectedProfiles[.user] }) { + self.bufferUser = self.currentCompany.users[i] + } + } + self.editingProfile = true + } + + func setBufferAvatar(_ img: String, for type: UserTypeDeprecated) { + switch type { + case .teacher: self.bufferTeacher.avatar = img + case .user: self.bufferUser.avatar = img + } + } + + func resetBufferAvatar(_ type: UserTypeDeprecated) { + switch type { + case .teacher: self.bufferTeacher.avatar = "" + case .user: self.bufferUser.avatar = "" + } + } + + func saveProfileChanges(_ type: UserTypeDeprecated) { + switch type { + case .teacher: + if let i = currentCompany.teachers.firstIndex(where: { $0.id == bufferTeacher.id }) { + self.currentCompany.teachers[i] = self.bufferTeacher + } else { + self.addTeacherProfile() + } + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [self] in + self.resetBufferProfile(.teacher) + } + + case .user: + if let i = currentCompany.users.firstIndex(where: { $0.id == bufferUser.id }) { + self.currentCompany.users[i] = self.bufferUser + } else { + self.addUserProfile() + } + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [self] in + self.resetBufferProfile(.user) + } + } + self.editingProfile = false + } + + func addTeacherProfile() { + if self.bufferTeacher.avatar == DesignKitAsset.Avatars.accompanyingWhite.name { + self.bufferTeacher.avatar = DesignKitAsset.Avatars.accompanyingBlue.name + } + self.currentCompany.teachers.insert(self.bufferTeacher, at: 0) + self.selectedProfiles[.teacher] = self.bufferTeacher.id + } + + func addUserProfile() { + if self.bufferUser.avatar == DesignKitAsset.Avatars.userWhite.name { + self.bufferUser.avatar = DesignKitAsset.Avatars.userBlue.name + } + self.currentCompany.users.insert(self.bufferUser, at: 0) + self.selectedProfiles[.user] = self.bufferUser.id + } + + func deleteProfile(_ type: UserTypeDeprecated) { + switch type { + case .teacher: + self.currentCompany.teachers.removeAll(where: { self.bufferTeacher.id == $0.id }) + self.profilesInUse[.teacher] = UUID() + self.selectedProfiles[.teacher] = UUID() + self.editingProfile = false + + case .user: + self.currentCompany.users.removeAll(where: { self.bufferUser.id == $0.id }) + self.profilesInUse[.user] = UUID() + self.selectedProfiles[.user] = UUID() + self.editingProfile = false + } + } + + // MARK: - DiscoveryMode + + func setupDiscoveryCompany() { + self.currentCompany = DiscoveryCompany().discoveryCompany + self.profilesInUse[.teacher] = self.currentCompany.teachers[0].id + self.profilesInUse[.user] = self.currentCompany.users[0].id + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Stores/CurriculumViewModel+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Stores/CurriculumViewModel+DEPRECATED.swift new file mode 100644 index 0000000000..303666ab4e --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Stores/CurriculumViewModel+DEPRECATED.swift @@ -0,0 +1,95 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI +import Yams + +class CurriculumViewModelDeprecated: ObservableObject, YamlFileDecodable { + // MARK: - CurriculumList Published properties + + @Published var currentCurriculumCategory: CurriculumCategories = .emotionRecognition + @Published var availableCurriculums: [Curriculum] = [] + + // MARK: - Current || selected Curriculum Published properties + + @Published var currentCurriculum = Curriculum() + @Published var currentCurriculumSelectedActivityID: UUID? + @Published var selectedCurriculumHeaderTitle: String = "" + @Published var selectedCurriculumRank: String = "" + @Published var selectedCurriculumIcon: String = "" + @Published var selectedCurriculumDescription: String = "" + + // MARK: - ActivityList -> will not stay here - list activities files from the Bundle instead + + @Published var activityFilesCompleteList: [String] = [] // will not stay like that - list files instead + + @Published var selectedCurriculum: Int? = 0 { + didSet { + self.currentCurriculum = self.availableCurriculums[self.selectedCurriculum ?? 0] + self.selectedCurriculumRank = "\(String(describing: (self.selectedCurriculum ?? 0) + 1))/\(self.availableCurriculums.count)" + self.selectedCurriculumHeaderTitle = self.availableCurriculums[self.selectedCurriculum ?? 0].fullTitle.localized() + self.selectedCurriculumIcon = self.setCurriculumIcon(for: self.currentCurriculum) // from Yaml later + self.selectedCurriculumDescription = // swiftlint:disable:next line_length + "Reconnaissance des 5 émotions primaires \n(peur, joie, tristesse, colère et dégoût) \nà travers les photos de 5 personnes différentes." + } + } + + // MARK: - CurriculumList related Work + + func getCurriculumList(category: CurriculumCategories) -> CurriculumList { + do { + return try decodeYamlFile(withName: category.rawValue, toType: CurriculumList.self) + } catch { + print("Failed to decode Yaml file with error:", error) + return CurriculumList() + } + } + + func populateCurriculumList(category: CurriculumCategories) { + self.availableCurriculums.removeAll() + for item in self.getCurriculumList(category: category).curriculums { + self.availableCurriculums.append(self.getCurriculum(item)) + } + } + + func getCurriculumsFrom(category: CurriculumCategories) -> [Curriculum] { + var curriculums: [Curriculum] = [] + for item in self.getCurriculumList(category: category).curriculums { + curriculums.append(self.getCurriculum(item)) + } + return curriculums + } + + // MARK: - Curriculum-Specific Work + + func getCurriculum(_ title: String) -> Curriculum { + do { + return try decodeYamlFile(withName: title, toType: Curriculum.self) + } catch { + print("Failed to decode Yaml file with error:", error) + return Curriculum() + } + } + + func setCurriculumDetailNavTitle() -> String { + "\(self.getCurriculumList(category: self.currentCurriculumCategory).sectionTitle.localized()) \(String(describing: (self.selectedCurriculum ?? 0) + 1))/\(self.availableCurriculums.count)" + } + + func setCurriculumIcon(for curriculum: Curriculum) -> String { + switch curriculum.id { + case "ec6fca8d-ac0f-44f8-b641-c9a96f9195c5": "parcours_Emotion_Recognition_Pictures" + case "7859be5a-9fa5-11ec-b909-0242ac120002": "parcours_Emotion_Recognition_Images" + case "6d41484b-71ff-4556-a821-ad85ad107c80": "parcours_Emotion_Recognition_Pictograms" + case "14a71d61-ed35-4122-b7a4-0a8895e06386": "parcours_Emotion_Recognition_Generalization" + case "a3a4aa6a-1ea5-4a8f-82cd-f3879cfbdc72": "parcours_Emotion_Recognition_Sounds" + default: "parcours_Emotion_Recognition_Pictures" + } + } + + func getCompleteActivityList() { + for curriculum in self.availableCurriculums { + self.activityFilesCompleteList.append(contentsOf: curriculum.activities) + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Stores/NavigationViewModel+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Stores/NavigationViewModel+DEPRECATED.swift new file mode 100644 index 0000000000..c46f0a9b57 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Stores/NavigationViewModel+DEPRECATED.swift @@ -0,0 +1,91 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import SwiftUI + +class NavigationViewModelDeprecated: ObservableObject { + // Educative Content section data + static let educContentSectionLabels: [SectionLabel] = [ + SectionLabel( + destination: .curriculums, + icon: "graduationcap", + label: "Parcours" + ), + SectionLabel( + destination: .activities, + icon: "dice", + label: "Activités" + ), + SectionLabel( + destination: .commands, + icon: "gamecontroller", + label: "Commandes" + ), + ] + + // sidebar utils + @Published var sidebarVisibility = NavigationSplitViewVisibility.all + @Published var showSettings: Bool = false + @Published var showProfileEditor: Bool = false + @Published var showRobotPicker: Bool = false + + @Published var educContentList = ListModel( + title: "Contenu Éducatif", + sections: educContentSectionLabels + ) + + // Overall Navigation from the sidebar + @Published var currentView: SidebarDestinations = .curriculums + // Navigation within FullScreenCover to GameView() + @Published var pathsFromHome = NavigationPath() + @Published var showActivitiesFullScreenCover: Bool = false + @Published var pathToGame = NavigationPath() + + // Info Tiles triggers + @Published var showInfoCurriculums: Bool = true + @Published var showInfoActivities: Bool = true + @Published var showInfoCommands: Bool = true + + // Returned Views & NavigationTitles + @ViewBuilder var allSidebarDestinationViews: some View { + switch self.currentView { + case .curriculums: CurriculumListViewDeprecated() + case .activities: ActivityListViewDeprecated() + case .commands: CommandListViewDeprecated() + } + } + + func getNavTitle() -> String { + switch self.currentView { + case .curriculums: "Parcours" + case .activities: "Activités" + case .commands: "Commandes" + } + } + + func contextualInfo() -> TileData { + switch self.currentView { + case .curriculums: .curriculums + case .activities: .activities + case .commands: .commands + } + } + + func showInfo() -> Bool { + switch self.currentView { + case .curriculums: self.showInfoCurriculums + case .activities: self.showInfoActivities + case .commands: self.showInfoCommands + } + } + + func updateShowInfo() { + switch self.currentView { + case .curriculums: self.showInfoCurriculums.toggle() + case .activities: self.showInfoActivities.toggle() + case .commands: self.showInfoCommands.toggle() + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Stores/RobotViewModel.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Stores/RobotViewModel.swift new file mode 100644 index 0000000000..11ca7f568d --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Stores/RobotViewModel.swift @@ -0,0 +1,27 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +class RobotViewModel: ObservableObject { + // Robot Connect + // Make 'robotIsConnected' & 'currentlyConnectedRobotIndex' one prop' instead + // if currentlyConnectedRobotIndex is not nil, robot is connected for sure + @Published var currentlySelectedRobotIndex: Int? + @Published var currentlyConnectedRobotIndex: Int? + + // robot Advertised Information + @Published var robotIsConnected: Bool = false + @Published var userChoseToPlayWithoutRobot: Bool = false + @Published var robotChargeLevel: Double = 100 + @Published var robotIsCharging: Bool = false + @Published var currentlyConnectedRobotName: String = "" + @Published var robotOSVersion: String = "LekaOS v1.4.0" + + func disconnect() { + self.currentlySelectedRobotIndex = nil + self.currentlyConnectedRobotIndex = nil + self.robotIsConnected = false + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Stores/SettingsViewModel+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Stores/SettingsViewModel+DEPRECATED.swift new file mode 100644 index 0000000000..8f39fe2807 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Stores/SettingsViewModel+DEPRECATED.swift @@ -0,0 +1,19 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +class SettingsViewModelDeprecated: ObservableObject { + // Connexion-related properties - Settings + @Published var companyIsLoggingIn: Bool = false + @Published var companyIsConnected: Bool = false + @Published var exploratoryModeIsOn: Bool = false + @Published var showSwitchOffExploratoryAlert: Bool = false + + @Published var showConfirmDisconnection: Bool = false + @Published var showConfirmDeleteAccount: Bool = false + + // This will go later in UIEvents Environment + @Published var showConnectInvite: Bool = false +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Stores/ViewRouter+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Stores/ViewRouter+DEPRECATED.swift new file mode 100644 index 0000000000..e479fcc8ad --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Stores/ViewRouter+DEPRECATED.swift @@ -0,0 +1,18 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// MARK: - Page + +enum Page { + case welcome + case home +} + +// MARK: - ViewRouterDeprecated + +class ViewRouterDeprecated: ObservableObject { + @Published var currentPage: Page = .welcome +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Styles/Styles+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Styles/Styles+DEPRECATED.swift new file mode 100644 index 0000000000..066a222460 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Styles/Styles+DEPRECATED.swift @@ -0,0 +1,223 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import Foundation +import SwiftUI + +// MARK: - NoFeedback_ButtonStyleDeprecated + +struct NoFeedback_ButtonStyleDeprecated: ButtonStyle { + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + } +} + +// MARK: - BorderedCapsule_NoFeedback_ButtonStyleDeprecated + +struct BorderedCapsule_NoFeedback_ButtonStyleDeprecated: ButtonStyle { + var font: Font + var color: Color + var isOpaque: Bool = false + var width: CGFloat = 280 + + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + .font(self.font) + .foregroundColor(self.color) + .padding(.vertical, 10) + .padding(.horizontal, 20) + .frame(width: self.width) + .overlay( + Capsule() + .stroke(self.color, lineWidth: 1) + ) + .background(self.isOpaque ? .white : .clear, in: Capsule()) + .contentShape(Capsule()) + } +} + +// MARK: - CircledIcon_NoFeedback_ButtonStyleDeprecated + +struct CircledIcon_NoFeedback_ButtonStyleDeprecated: ButtonStyle { + var font: Font + + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + .font(self.font) + .foregroundColor(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) + .frame(width: 46, height: 46) + .overlay( + Circle() + .stroke(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor, lineWidth: 1) + ) + .contentShape(Circle()) + } +} + +// MARK: - Connect_ButtonStyleDeprecated + +struct Connect_ButtonStyleDeprecated: ButtonStyle { + @EnvironmentObject var metrics: UIMetrics + var reversed: Bool = false + + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + .font(.body) + .frame(width: 310, height: 60) + .foregroundColor(self.reversed ? DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor : .white) + .background(self.reversed ? .white : DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor, in: Capsule()) + .contentShape(Capsule()) + .opacity(configuration.isPressed ? 0.4 : 1) + .compositingGroup() + .animation(.easeIn(duration: 0.05), value: configuration.isPressed) + } +} + +// MARK: - JobPickerToggleStyleDeprecated + +struct JobPickerToggleStyleDeprecated: ToggleStyle { + @EnvironmentObject var metrics: UIMetrics + var onImage = "checkmark.circle" + var offImage = "circle" + var action: () -> Void + + func makeBody(configuration: Configuration) -> some View { + HStack(spacing: 10) { + Image(systemName: configuration.isOn ? self.onImage : self.offImage) + .foregroundColor(configuration.isOn ? .white : DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) + .background( + Circle() + .inset(by: 2) + .fill(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) + .opacity(configuration.isOn ? 1 : 0) + .scaleEffect(configuration.isOn ? 1 : 0.1, anchor: .center) + ) + .scaleEffect(configuration.isOn ? 1.2 : 1, anchor: .center) + .frame(width: 18, height: 18) + + configuration.label + .multilineTextAlignment(.leading) + .font(.body) + + Spacer() + } + .frame(width: 200) + .onTapGesture { + withAnimation(.easeIn(duration: 0.2)) { + configuration.isOn.toggle() + self.action() + } + } + } +} + +// MARK: - SuccessGaugeStyleDeprecated + +struct SuccessGaugeStyleDeprecated: GaugeStyle { + func makeBody(configuration: Configuration) -> some View { + let color: Color = if configuration.value < 0.25 { + .red + } else if 0.25..<0.5 ~= configuration.value { + .orange + } else if 0.5..<0.8 ~= configuration.value { + .yellow + } else { + .green + } + + ZStack { + Circle() + .stroke(color.opacity(0.4), lineWidth: 6) + .frame(maxWidth: 54) + Circle() + .trim(from: 0, to: CGFloat(configuration.value)) + .stroke(color, style: StrokeStyle(lineWidth: 6, lineCap: .round)) + .frame(maxHeight: 54) + .rotationEffect(Angle(degrees: -90)) + Text("\(Int(configuration.value * 100))%") + } + } +} + +// MARK: - ActivityAnswer_ButtonStyleDeprecated + +struct ActivityAnswer_ButtonStyleDeprecated: ButtonStyle { + var isEnabled: Bool = false + + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + .scaleEffect(configuration.isPressed ? 0.98 : 1, anchor: .center) + .mask(Circle().inset(by: 4)) + .background( + Circle() + .fillDeprecated( + DesignKitAsset.Colors.gameButtonBorder.swiftUIColor, + strokeBorder: DesignKitAsset.Colors.gameButtonBorder.swiftUIColor, + lineWidth: 4 + ) + ) + .overlay( + Circle() + .fill(self.isEnabled ? .clear : .white.opacity(0.6)) + ) + .contentShape(Circle()) + .animation(.easeOut(duration: 0.3), value: self.isEnabled) + } +} + +// MARK: - PlaySound_ButtonStyleDeprecated + +struct PlaySound_ButtonStyleDeprecated: ButtonStyle { + var progress: CGFloat + + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + .mask(Circle().inset(by: 4)) + .background( + Circle() + .fillDeprecated( + Color.white, + strokeBorder: DesignKitAsset.Colors.gameButtonBorder.swiftUIColor, + lineWidth: 4 + ) + .overlay( + Circle() + .trim(from: 0, to: self.progress) + .stroke( + DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor, + style: StrokeStyle(lineWidth: 10, lineCap: .round) + ) + .rotationEffect(.degrees(-90)) + .animation(.easeOut(duration: 0.2), value: self.progress) + ) + ) + .contentShape(Circle()) + } +} + +// MARK: - BorderedCapsule_ButtonStyleDeprecated + +struct BorderedCapsule_ButtonStyleDeprecated: ButtonStyle { + var isFilled: Bool = true + + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + .font(.body) + .foregroundColor(!self.isFilled ? DesignKitAsset.Colors.bravoHighlights.swiftUIColor : .white) + .opacity(configuration.isPressed ? 0.95 : 1) + .scaleEffect(configuration.isPressed ? 0.99 : 1, anchor: .center) + .padding() + .frame(width: 250) + .background( + Capsule() + .fillDeprecated( + self.isFilled ? DesignKitAsset.Colors.bravoHighlights.swiftUIColor : .white, + strokeBorder: DesignKitAsset.Colors.bravoHighlights.swiftUIColor, + lineWidth: 1 + ) + .shadow(color: .black.opacity(0.1), radius: 2.3, x: 0, y: 1.8) + ) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Styles/UIMetrics.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Styles/UIMetrics.swift new file mode 100644 index 0000000000..a50c17cc54 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Styles/UIMetrics.swift @@ -0,0 +1,36 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import SwiftUI + +class UIMetrics: ObservableObject { + // MARK: - Global + + @Published var btnRadius: CGFloat = 10 + + // MARK: - Curriculums + + // PillShaped View + @Published var pillRadius: CGFloat = 70 + + // MARK: - Avatars + + @Published var diameter: CGFloat = 125 + + // MARK: - Settings + + @Published var verticalPadding: CGFloat = 6 + + // MARK: - Tiles + + @Published var tilesRadius: CGFloat = 21 + @Published var tileBtnWidth: CGFloat = 280 + @Published var tilePictoHeightSmall: CGFloat = 80 + @Published var tilePictoHeightMedium: CGFloat = 100 + @Published var tilePictoHeightBig: CGFloat = 120 + @Published var tileContentWidth: CGFloat = 360 + @Published var tileContentPadding: CGFloat = 25 + @Published var tileSize = CGSize(width: 843, height: 327) +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/UserTypes/CustomTypes.swift b/Apps/LekaApp/Sources/_OLDCodeBase/UserTypes/CustomTypes.swift new file mode 100644 index 0000000000..a610f22ea5 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/UserTypes/CustomTypes.swift @@ -0,0 +1,36 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import SwiftUI + +// MARK: - FormField + +enum FormField { + case mail + case password + case confirm + case name +} + +// MARK: - LocalizedContent + +struct LocalizedContent: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case enUS = "en_US" + case frFR = "fr_FR" + } + + var enUS: String? + var frFR: String? +} + +// MARK: - ResultType + +enum ResultType { + case idle + case fail + case medium + case success +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities/ActivityListView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities/ActivityListView+DEPRECATED.swift new file mode 100644 index 0000000000..c81dff8b98 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities/ActivityListView+DEPRECATED.swift @@ -0,0 +1,99 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct ActivityListViewDeprecated: View { + // MARK: Internal + + @EnvironmentObject var curriculumVM: CurriculumViewModelDeprecated + @EnvironmentObject var activityVM: ActivityViewModelDeprecated + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + // Data modeled for Search Feature + @State var searchQuery = "" + + var searchResults: [String] { + guard self.searchQuery.isEmpty else { + return self.curriculumVM.activityFilesCompleteList.filter { + self.activityVM.getActivity($0).title.localized().localizedCaseInsensitiveContains(self.searchQuery) + // || $0.texts[1].localizedCaseInsensitiveContains(searchQuery) + // || $0.texts[3].localizedCaseInsensitiveContains(searchQuery) + } + // filters are titles & keywords (added later), and in curriculums => subtitle, short + // later add played/unplayed, last played, sound only etc... + } + return self.curriculumVM.activityFilesCompleteList + } + + var body: some View { + ZStack { + DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea() + self.completeActivityList + } + .animation(.easeOut(duration: 0.4), value: self.navigationVM.showInfo()) + .searchable( + text: self.$searchQuery, + placement: .toolbar, + prompt: Text("Media, personnages, ...") + ) + .onAppear { self.navigationVM.sidebarVisibility = .all } + .navigationDestination( + for: String.self, + destination: { _ in + SelectedActivityInstructionsViewDeprecated() + } + ) + } + + // MARK: Private + + private var completeActivityList: some View { + ScrollViewReader { proxy in + List(self.searchResults.enumerated().map { $0 }, id: \.element) { index, item in + HStack { + Spacer() + Button { + self.activityVM.currentActivity = self.activityVM.getActivity(item) + self.activityVM.selectedActivityID = UUID(uuidString: self.activityVM.getActivity(item).id) + self.navigationVM.pathsFromHome.append("instructions") + } label: { + ActivityListCellDeprecated( + activity: self.activityVM.getActivity(item), + icon: item, + rank: index + 1, + selected: self.activityVM.selectedActivityID == UUID(uuidString: self.activityVM.getActivity(item).id) + ) + } + .alignmentGuide(.listRowSeparatorLeading) { _ in + 0 + } + .frame(maxWidth: 600) + .buttonStyle(NoFeedback_ButtonStyleDeprecated()) + .contentShape(Rectangle()) + Spacer() + } + .id(UUID(uuidString: self.activityVM.getActivity(item).id)) + } + .listStyle(PlainListStyle()) + .padding(.bottom, 20) + .background(.white, in: Rectangle()) + .edgesIgnoringSafeArea(.bottom) + .animation(.default, value: self.searchQuery) + .safeAreaInset(edge: .top) { + InfoTileManagerDeprecated() + } + .onAppear { + guard self.activityVM.selectedActivityID != nil else { + return + } + withAnimation { proxy.scrollTo(self.activityVM.selectedActivityID, anchor: .center) } + } + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities/Components/ActivityListCell+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities/Components/ActivityListCell+DEPRECATED.swift new file mode 100644 index 0000000000..01a967571e --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities/Components/ActivityListCell+DEPRECATED.swift @@ -0,0 +1,55 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct ActivityListCellDeprecated: View { + // MARK: Internal + + @EnvironmentObject var metrics: UIMetrics + + let activity: ActivityDeprecated + let icon: String + let iconDiameter: CGFloat = 132 + let rank: Int + let selected: Bool + + var body: some View { + HStack(spacing: 20) { + self.iconView + self.cellContent + Spacer() + } + .frame(minWidth: 420, maxHeight: self.iconDiameter + 20) + .background(self.selected ? Color.accentColor : .white) // TODO: (@ui/ux) - nil might be better here + .clipShape(RoundedRectangle(cornerRadius: self.metrics.btnRadius, style: .continuous)) + .padding(.vertical, 4) + } + + // MARK: Private + + private var iconView: some View { + Image(self.icon) + .activityIconImageModifier(diameter: self.iconDiameter) + .padding(.leading, 10) + } + + private var cellContent: some View { + VStack(alignment: .leading, spacing: 0) { + Spacer() + Text(self.activity.title.localized()) + .font(.title2) + Spacer() + Group { + Text("ACTIVITÉ \(self.rank)") + .font(.headline) + + Text(" - \(self.activity.short.localized())") + } + .multilineTextAlignment(.leading) + .padding(.bottom, 10) + } + .foregroundColor(self.selected ? .white : Color.accentColor) // TODO: (@ui/ux) - nil might be better here + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities/Components/SelectedActivityInstructionsViewDeprecated+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities/Components/SelectedActivityInstructionsViewDeprecated+DEPRECATED.swift new file mode 100644 index 0000000000..89d168bbd7 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities/Components/SelectedActivityInstructionsViewDeprecated+DEPRECATED.swift @@ -0,0 +1,47 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct SelectedActivityInstructionsViewDeprecated: View { + // MARK: Internal + + @EnvironmentObject var activityVM: ActivityViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + ZStack(alignment: .top) { + // NavigationBar color + DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea() + + // Background Color (only visible under the header here) + DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor + + VStack(spacing: 0) { + self.activityDetailHeader + Rectangle() + .fill(DesignKitAsset.Colors.lekaLightGray.swiftUIColor) + .edgesIgnoringSafeArea(.bottom) + .overlay { InstructionsViewDeprecated() } + .overlay { GoButtonDeprecated() } + } + } + .preferredColorScheme(.light) + } + + // MARK: Private + + private var activityDetailHeader: some View { + HStack { + Spacer() + Text(self.activityVM.currentActivity.title.localized()) + .font(.headline) + .foregroundColor(.white) + .multilineTextAlignment(.center) + Spacer() + } + .frame(width: 420, height: 90) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities_shared/GoButton+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities_shared/GoButton+DEPRECATED.swift new file mode 100644 index 0000000000..5f6d3d0df3 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities_shared/GoButton+DEPRECATED.swift @@ -0,0 +1,77 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct GoButtonDeprecated: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var activityVM: ActivityViewModelDeprecated + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + @EnvironmentObject var robotVM: RobotViewModel + @EnvironmentObject var settings: SettingsViewModelDeprecated + + var body: some View { + VStack { + HStack(alignment: .top) { + Spacer() + Button { + self.goButtonAction() + } label: { + self.goButtonLabel + } + .background(DesignKitAsset.Colors.lekaLightGray.swiftUIColor, in: Circle()) + .padding(.trailing, 40) + } + .offset(y: -40) + Spacer() + } + } + + // MARK: Private + + private var goButtonLabel: some View { + ZStack { + Circle() + .inset(by: 6) + .fill(DesignKitAsset.Colors.lekaLightBlue.swiftUIColor) + .shadow(color: .black.opacity(0.1), radius: 2.5, x: 0, y: 2.5) + Circle() + .inset(by: 8) + .fill(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) + .shadow(color: .black.opacity(0.2), radius: 2.5, x: 0, y: 2.6) + Circle() + .inset(by: 15) + .stroke(.white, lineWidth: 2) + Text("GO !") + .foregroundColor(.white) + .font(.system(size: 34, weight: .bold, design: .rounded)) + } + .frame(width: 127, height: 127) + .compositingGroup() + } + + private func goButtonAction() { + self.activityVM.setupGame(with: self.activityVM.currentActivity) + guard self.robotVM.robotIsConnected || self.robotVM.userChoseToPlayWithoutRobot else { + self.navigationVM.pathToGame = NavigationPath([PathsToGame.robot]) + self.navigationVM.showActivitiesFullScreenCover = true + return + } + guard self.settings.companyIsConnected else { + self.navigationVM.pathToGame = NavigationPath([PathsToGame.game]) + self.navigationVM.showActivitiesFullScreenCover = true + return + } + guard self.company.selectionSetIsCorrect() else { + self.navigationVM.pathToGame = NavigationPath([PathsToGame.user]) + self.navigationVM.showActivitiesFullScreenCover = true + return + } + self.navigationVM.pathToGame = NavigationPath([PathsToGame.game]) + self.navigationVM.showActivitiesFullScreenCover = true + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities_shared/InstructionsView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities_shared/InstructionsView+DEPRECATED.swift new file mode 100644 index 0000000000..9eb5aed430 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities_shared/InstructionsView+DEPRECATED.swift @@ -0,0 +1,62 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// TODO(@ladislas): reimport when Down is fixed +// import Down +import DesignKit +import SwiftUI + +// MARK: - InstructionsViewDeprecated + +struct InstructionsViewDeprecated: View { + // MARK: Internal + + @EnvironmentObject var activityVM: ActivityViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + ScrollView(.vertical, showsIndicators: true) { + // instructions_OLD + self.instructionsMarkdownView + } + .safeAreaInset(edge: .top) { + self.instructionTitle + } + } + + // MARK: Private + + @ViewBuilder + private var instructionsMarkdownView: some View { + // Text(activityVM.getInstructions()) + DownAttributedString(text: self.activityVM.getInstructions()) + // MarkdownRepresentable(height: .constant(.zero)) + .environmentObject(MarkdownObservable(text: self.activityVM.getInstructions())) + .padding() + .frame(minWidth: 450, maxWidth: 550) + } + + private var instructionTitle: some View { + HStack { + Spacer() + Text("DESCRIPTION & INSTALLATION") + .font(.headline) + .foregroundColor(DesignKitAsset.Colors.darkGray.swiftUIColor.opacity(0.8)) + .padding(.vertical, 22) + Spacer() + } + .padding(.top, 30) + .background(DesignKitAsset.Colors.lekaLightGray.swiftUIColor) + } +} + +// MARK: - InstructionsView_Previews + +struct InstructionsView_Previews: PreviewProvider { + static var previews: some View { + InstructionsViewDeprecated() + .environmentObject(UIMetrics()) + .environmentObject(ActivityViewModelDeprecated()) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities_shared/MarkdownRepresentable.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities_shared/MarkdownRepresentable.swift new file mode 100644 index 0000000000..73ab68e3f9 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Activities_shared/MarkdownRepresentable.swift @@ -0,0 +1,146 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// +// DownAttributedString.swift +// Down-SwiftUI-Example +// +// Created by Mikhail Ivanov on 01.06.2021. +// Copyright © 2021 Down. All rights reserved. +// +import SwiftUI + +// MARK: - MarkdownObservable + +// TODO(@ladislas): reimport when Down is fixed +// import Down + +class MarkdownObservable: ObservableObject { + // MARK: Lifecycle + + init(text: String) { + self.text = text + } + + // MARK: Public + + @Published public var textView = UITextView() + public let text: String +} + +// MARK: - MarkdownRepresentable + +struct MarkdownRepresentable: UIViewRepresentable { + // MARK: Lifecycle + + init(height: Binding) { + _dynamicHeight = height + } + + // MARK: Internal + + class Coordinator: NSObject { + // MARK: Lifecycle + + init(text: UITextView) { + self.textView = text + } + + // func textAttachmentDidLoadImage(textAttachment: AsyncImageLoad, displaySizeChanged: Bool) + // { + // if displaySizeChanged + // { + // textView.layoutManager.setNeedsLayout(forAttachment: textAttachment) + // } + // + // // always re-display, the image might have changed + // textView.layoutManager.setNeedsDisplay(forAttachment: textAttachment) + // } + + // MARK: Public + + // }, AsyncImageLoadDelegate { + public var textView: UITextView + } + + @Binding var dynamicHeight: CGFloat + @EnvironmentObject var markdownObject: MarkdownObservable + + func makeCoordinator() -> Coordinator { + Coordinator(text: self.markdownObject.textView) + } + + func makeUIView(context _: Context) -> UITextView { + // TODO(@ladislas): reimport when Down is fixed + // let down = Down(markdownString: markdownObject.text) + // let attributedText = try? down.toAttributedString(styler: DownStyler())//delegate: context.coordinator)) + + // TODO(@ladislas): reimport when Down is fixed + // let attributedText = try? down.toAttributedString(styler: DownStyler()) + let attributedText = NSMutableAttributedString( + string: "TODO(@ladislas): use real markdown when Down is fixed") + + self.markdownObject.textView.attributedText = attributedText + self.markdownObject.textView.textAlignment = .left + self.markdownObject.textView.isScrollEnabled = false + self.markdownObject.textView.isUserInteractionEnabled = true + self.markdownObject.textView.showsVerticalScrollIndicator = false + self.markdownObject.textView.showsHorizontalScrollIndicator = false + self.markdownObject.textView.isEditable = false + self.markdownObject.textView.backgroundColor = .clear + self.markdownObject.textView.textColor = UIColor(named: "darkGray") + // markdownObject.textView.font = UIFont(name: "SF Pro Regular", size: 14) + + self.markdownObject.textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + self.markdownObject.textView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + + return self.markdownObject.textView + } + + func updateUIView(_ uiView: UITextView, context _: Context) { + DispatchQueue.main.async { + // uiView.textColor = UIColor(named: "darkGray") + + self.dynamicHeight = + uiView.sizeThatFits( + CGSize( + width: uiView.bounds.width, + height: CGFloat.greatestFiniteMagnitude + ) + ) + .height + } + } +} + +// MARK: - DownAttributedString + +struct DownAttributedString: View { + // MARK: Lifecycle + + init(text: String) { + self.markdownString = text + self.markdownObject = MarkdownObservable(text: text) + } + + // MARK: Internal + + var body: some View { + VStack(alignment: .leading) { + ScrollView { + MarkdownRepresentable(height: self.$height) + .frame(height: self.height) + .environmentObject(self.markdownObject) + } + } + .navigationBarTitleDisplayMode(.inline) + } + + // MARK: Private + + @ObservedObject private var markdownObject: MarkdownObservable + private var markdownString: String + + @State private var height: CGFloat = .zero +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Commands/CommandListView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Commands/CommandListView+DEPRECATED.swift new file mode 100644 index 0000000000..fe84c91822 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Commands/CommandListView+DEPRECATED.swift @@ -0,0 +1,60 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - CommandListViewDeprecated + +struct CommandListViewDeprecated: View { + // MARK: Internal + + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + + var body: some View { + ZStack { + DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea() + + let columns = Array(repeating: GridItem(), count: 3) + VStack { + LazyVGrid(columns: columns) { + ForEach(self.images.indices, id: \.self) { item in + Image(self.images[item]) + .activityIconImageModifier(padding: 20) + .padding() + } + } + .safeAreaInset(edge: .top) { + if self.settings.companyIsConnected, !self.navigationVM.showInfo() { + Color.clear + .frame(height: self.settings.companyIsConnected ? 40 : 0) + } else { + InfoTileManagerDeprecated() + } + } + Spacer() + } + } + .animation(.easeOut(duration: 0.4), value: self.navigationVM.showInfo()) + .onAppear { self.navigationVM.sidebarVisibility = .all } + } + + // MARK: Private + + private let images: [String] = [ + "standard-remote", "colored-arrows", "color-remote copy", "big-joystick", "hand-remote", + ] +} + +// MARK: - CommandListView_Previews + +struct CommandListView_Previews: PreviewProvider { + static var previews: some View { + CommandListViewDeprecated() + .environmentObject(NavigationViewModelDeprecated()) + .environmentObject(SettingsViewModelDeprecated()) + .environmentObject(UIMetrics()) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Curriculums/Components/ActivityListCell_Curriculums+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Curriculums/Components/ActivityListCell_Curriculums+DEPRECATED.swift new file mode 100644 index 0000000000..57ba7be026 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Curriculums/Components/ActivityListCell_Curriculums+DEPRECATED.swift @@ -0,0 +1,55 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct ActivityListCell_CurriculumsDeprecated: View { + // MARK: Internal + + @EnvironmentObject var metrics: UIMetrics + + let activity: ActivityDeprecated + let icon: String + let iconDiameter: CGFloat = 100 + let rank: Int + let selected: Bool + + var body: some View { + HStack(spacing: 20) { + self.iconView + self.cellContent + Spacer() + } + .frame(minWidth: 420, maxHeight: self.iconDiameter + 20) + .background(self.selected ? DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor : .white) + .clipShape(RoundedRectangle(cornerRadius: self.metrics.btnRadius, style: .continuous)) + .padding(.vertical, 4) + } + + // MARK: Private + + private var iconView: some View { + Image(self.icon) + .activityIconImageModifier(diameter: self.iconDiameter) + .padding(.leading, 10) + } + + private var cellContent: some View { + VStack(alignment: .leading, spacing: 0) { + Spacer() + Text(self.activity.title.localized()) + .font(.title2) + Spacer() + Group { + Text("ACTIVITÉ \(self.rank)") + .font(.headline) + + Text(" - \(self.activity.short.localized())") + } + .multilineTextAlignment(.leading) + .padding(.bottom, 10) + } + .foregroundColor(self.selected ? .white : DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Curriculums/Components/CurriculumPillShapedView.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Curriculums/Components/CurriculumPillShapedView.swift new file mode 100644 index 0000000000..5378147865 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Curriculums/Components/CurriculumPillShapedView.swift @@ -0,0 +1,57 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct CurriculumPillShapedView: View { + // MARK: Internal + + @EnvironmentObject var metrics: UIMetrics + + var curriculum: Curriculum + var icon: String + + var body: some View { + VStack(spacing: 0) { + self.topContent + self.bottomContent + } + .frame(width: 200, height: 240) + .background(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor, in: Rectangle()) + .clipShape(RoundedRectangle(cornerRadius: self.metrics.pillRadius, style: .continuous)) + .shadow(color: .black.opacity(0.1), radius: 1, x: 0, y: 1) + .compositingGroup() + } + + // MARK: Private + + private var topContent: some View { + Text(self.curriculum.fullTitle.localized()) + .multilineTextAlignment(.center) + .font(.body) + .frame(maxWidth: 136, minHeight: 120) + .foregroundColor(.white) + } + + private var bottomContent: some View { + VStack(alignment: .center, spacing: 0) { + Spacer() + self.iconView + Spacer() + Text("\(self.curriculum.activities.count) activités") + .font(.caption) + .foregroundColor(DesignKitAsset.Colors.darkGray.swiftUIColor) + .padding(.bottom, 12) + } + .background(Color.white, in: Rectangle()) + } + + private var iconView: some View { + Image(self.icon) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 200, height: 70) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Curriculums/CurriculumDetailsView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Curriculums/CurriculumDetailsView+DEPRECATED.swift new file mode 100644 index 0000000000..a01e68e862 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Curriculums/CurriculumDetailsView+DEPRECATED.swift @@ -0,0 +1,148 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - CurriculumDetailsViewDeprecated + +struct CurriculumDetailsViewDeprecated: View { + // MARK: Internal + + @EnvironmentObject var curriculumVM: CurriculumViewModelDeprecated + @EnvironmentObject var activityVM: ActivityViewModelDeprecated + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + self.curriculumDetailContent + } + + // MARK: Private + + private var curriculumDetailContent: some View { + ZStack(alignment: .top) { + // NavigationBar color + DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea() + + // Background Color (only visible under the header here) + DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor + + VStack(spacing: 0) { + self.curriculumDetailHeader + HStack(spacing: 0) { + self.curriculumActivityList + // Instructions + GoBtn + Rectangle() + .fill(DesignKitAsset.Colors.lekaLightGray.swiftUIColor) + .edgesIgnoringSafeArea(.bottom) + .overlay { InstructionsViewDeprecated() } + .overlay { + GoButtonDeprecated() + .disabled(self.goButtonIsDisabled()) + } + } + } + } + .preferredColorScheme(.light) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden() + .toolbarBackground(.automatic, for: .navigationBar) + .onAppear { self.navigationVM.sidebarVisibility = .detailOnly } + .toolbar { + ToolbarItem(placement: .principal) { + Text(self.curriculumVM.setCurriculumDetailNavTitle()) + .font(.headline) + } + ToolbarItem(placement: .navigationBarLeading) { + Button( + action: { + self.navigationVM.pathsFromHome = .init() + }, + label: { + HStack(spacing: 4) { + Image(systemName: "chevron.left") + Text("Retour") + } + } + ) + } + } + } + + private var curriculumDetailHeader: some View { + HStack { + Spacer() + VStack(spacing: 20) { + Text(self.curriculumVM.selectedCurriculumHeaderTitle) + .font(.title2) + .padding(.top, 15) + Image(self.curriculumVM.selectedCurriculumIcon) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 200, height: 66) + Text(self.curriculumVM.selectedCurriculumDescription) + .font(.headline) + .multilineTextAlignment(.center) + Spacer() + } + .foregroundColor(.white) + Spacer() + } + .frame(height: 258) + } + + private var curriculumActivityList: some View { + ScrollViewReader { proxy in + List(self.curriculumVM.currentCurriculum.activities.enumerated().map { $0 }, id: \.element) { index, item in + Button { + self.curriculumVM.currentCurriculumSelectedActivityID = UUID(uuidString: self.activityVM.getActivity(item).id) + self.activityVM.currentActivity = self.activityVM.getActivity(item) + } label: { + ActivityListCell_CurriculumsDeprecated( + activity: self.activityVM.getActivity(item), + icon: item, + rank: index + 1, + selected: self.curriculumVM.currentCurriculumSelectedActivityID + == UUID(uuidString: self.activityVM.getActivity(item).id) + ) + } + .alignmentGuide(.listRowSeparatorLeading) { _ in + 0 + } + .buttonStyle(NoFeedback_ButtonStyleDeprecated()) + .contentShape(Rectangle()) + .id(UUID(uuidString: self.activityVM.getActivity(item).id)) + } + .listStyle(PlainListStyle()) + .padding(.bottom, 20) + .background(.white, in: Rectangle()) + .edgesIgnoringSafeArea([.bottom]) + .onAppear { + guard self.curriculumVM.currentCurriculumSelectedActivityID != nil else { + return + } + withAnimation { proxy.scrollTo(self.curriculumVM.currentCurriculumSelectedActivityID, anchor: .top) } + } + } + } + + private func goButtonIsDisabled() -> Bool { + !self.curriculumVM.currentCurriculum.activities.map { UUID(uuidString: self.activityVM.getActivity($0).id) } + .contains(self.curriculumVM.currentCurriculumSelectedActivityID) + } +} + +// MARK: - ContextualActivitiesDetailsView_Previews + +struct ContextualActivitiesDetailsView_Previews: PreviewProvider { + static var previews: some View { + CurriculumDetailsViewDeprecated() + .environmentObject(CurriculumViewModelDeprecated()) + .environmentObject(ActivityViewModelDeprecated()) + .environmentObject(UIMetrics()) + .environmentObject(NavigationViewModelDeprecated()) + .previewInterfaceOrientation(.landscapeLeft) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Curriculums/CurriculumListView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Curriculums/CurriculumListView+DEPRECATED.swift new file mode 100644 index 0000000000..9cb5ebc3d7 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/Curriculums/CurriculumListView+DEPRECATED.swift @@ -0,0 +1,95 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - CurriculumListViewDeprecated + +struct CurriculumListViewDeprecated: View { + // MARK: Internal + + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + @EnvironmentObject var curriculumVM: CurriculumViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + ZStack { + DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea() + + ScrollViewReader { proxy in + ScrollView { + LazyVGrid(columns: self.columns) { + ForEach(CurriculumCategories.allCases, id: \.self) { category in + Section { + self.allCurriculums(category: category) + } header: { + self.headerViews(title: self.curriculumVM.getCurriculumList(category: category).sectionTitle) + } + } + } + } + .animation(.easeOut(duration: 0.4), value: self.navigationVM.showInfo()) + .safeAreaInset(edge: .top) { + InfoTileManagerDeprecated() + } + .onAppear { + withAnimation { proxy.scrollTo(self.curriculumVM.currentCurriculumCategory, anchor: .top) } + } + } + } + .navigationDestination( + for: String.self, + destination: { _ in + CurriculumDetailsViewDeprecated() + } + ) + } + + // MARK: Private + + private let columns = Array(repeating: GridItem(), count: 3) + + @ViewBuilder + private func allCurriculums(category: CurriculumCategories) -> some View { + let list: [Curriculum] = self.curriculumVM.getCurriculumsFrom(category: category) + ForEach(list.enumerated().map { $0 }, id: \.element.id) { index, item in + Button { + self.curriculumVM.currentCurriculumCategory = category + self.curriculumVM.populateCurriculumList(category: category) + self.curriculumVM.selectedCurriculum = index + self.navigationVM.pathsFromHome.append("curriculumDetail") + } label: { + CurriculumPillShapedView( + curriculum: item, // Integrate rank and icon within curriculum Type, delete following properties + icon: self.curriculumVM.setCurriculumIcon(for: item) + ) + } + .padding() + } + } + + private func headerViews(title: LocalizedContent) -> some View { + HStack { + Text(title.localized()) + .font(.body) + .padding(16) + .padding(.leading, 20) + Spacer() + } + } +} + +// MARK: - CurriculumListView_Previews + +struct CurriculumListView_Previews: PreviewProvider { + static var previews: some View { + CurriculumListViewDeprecated() + .environmentObject(NavigationViewModelDeprecated()) + .environmentObject(CurriculumViewModelDeprecated()) + .environmentObject(UIMetrics()) + .environmentObject(SettingsViewModelDeprecated()) + .previewInterfaceOrientation(.landscapeLeft) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/PathToGameview/FullScreenCoverToGameView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/PathToGameview/FullScreenCoverToGameView+DEPRECATED.swift new file mode 100644 index 0000000000..8aff751549 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/EducationalContent/PathToGameview/FullScreenCoverToGameView+DEPRECATED.swift @@ -0,0 +1,37 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import RobotKit +import SwiftUI + +// MARK: - FullScreenCoverToGameViewDeprecated + +struct FullScreenCoverToGameViewDeprecated: View { + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + + var body: some View { + NavigationStack(path: self.$navigationVM.pathToGame) { + EmptyView() + .navigationDestination( + for: PathsToGame.self, + destination: { destination in + switch destination { + case .robot: RobotConnectionView() + case .user: ProfileSelector_UsersDeprecated() + case .game: GameViewDeprecated() + } + } + ) + } + } +} + +// MARK: - FullScreenCoverToGameView_Previews + +struct FullScreenCoverToGameView_Previews: PreviewProvider { + static var previews: some View { + FullScreenCoverToGameViewDeprecated() + .environmentObject(NavigationViewModelDeprecated()) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/Components/CurrentGameInstructionView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/Components/CurrentGameInstructionView+DEPRECATED.swift new file mode 100644 index 0000000000..60cc3a3010 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/Components/CurrentGameInstructionView+DEPRECATED.swift @@ -0,0 +1,80 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - CurrentGameInstructionViewDeprecated + +struct CurrentGameInstructionViewDeprecated: View { + // MARK: Internal + + @EnvironmentObject var activityVM: ActivityViewModelDeprecated + @ObservedObject var gameMetrics: GameMetrics + @Environment(\.dismiss) var dismiss + + var body: some View { + NavigationStack { + ZStack(alignment: .top) { + // Header color + DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor.ignoresSafeArea() + + // Background Color + DesignKitAsset.Colors.lekaLightGray.swiftUIColor.padding(.top, 70) + + VStack(spacing: 0) { + self.activityDetailHeader + InstructionsViewDeprecated() + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.hidden, for: .navigationBar) + .interactiveDismissDisabled() + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + self.resumeButton + } + } + } + .preferredColorScheme(.light) + } + + // MARK: Private + + private var activityDetailHeader: some View { + HStack { + Spacer() + Text(self.activityVM.currentActivity.title.localized()) + .font(.headline) + .foregroundColor(.white) + .multilineTextAlignment(.center) + Spacer() + } + .frame(height: 70) + .padding(.horizontal, 20) + } + + private var resumeButton: some View { + Button( + action: { + self.dismiss() + }, + label: { + HStack(spacing: 4) { + Image(systemName: "arrow.2.circlepath") + Text("Reprendre") + } + .foregroundColor(.white) + } + ) + } +} + +// MARK: - CurrentGameInstructionView_Previews + +struct CurrentGameInstructionView_Previews: PreviewProvider { + static var previews: some View { + CurrentGameInstructionViewDeprecated(gameMetrics: GameMetrics()) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/Components/InstructionButton+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/Components/InstructionButton+DEPRECATED.swift new file mode 100644 index 0000000000..850cebf77f --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/Components/InstructionButton+DEPRECATED.swift @@ -0,0 +1,87 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - InstructionButtonDeprecated + +struct InstructionButtonDeprecated: View { + @ObservedObject var gameMetrics: GameMetrics + @EnvironmentObject var activityVM: ActivityViewModelDeprecated + + var body: some View { + HStack(spacing: 0) { + Spacer() + Text(self.activityVM.steps[self.activityVM.currentStep].instruction.localized()) + .foregroundColor(DesignKitAsset.Colors.lekaDarkGray.swiftUIColor) + .font(.title2) + .multilineTextAlignment(.center) + .padding(.horizontal, self.gameMetrics.instructionFrame.height) + Spacer() + } + .frame(maxWidth: self.gameMetrics.instructionFrame.width) + .frame(height: self.gameMetrics.instructionFrame.height, alignment: .center) + .background( + ZStack { + Color.white + LinearGradient( + gradient: Gradient(colors: [.black.opacity(0.1), .black.opacity(0.0), .black.opacity(0.0)]), + startPoint: .top, endPoint: .center + ) + .opacity(self.activityVM.isSpeaking ? 1 : 0) + } + ) + .overlay( + RoundedRectangle(cornerRadius: self.gameMetrics.roundedCorner, style: .circular) + .fillDeprecated( + .clear, + strokeBorder: LinearGradient( + gradient: Gradient(colors: [.black.opacity(0.2), .black.opacity(0.05)]), startPoint: .bottom, + endPoint: .top + ), lineWidth: 4 + ) + .opacity(self.activityVM.isSpeaking ? 0.5 : 0) + ) + .overlay( + HStack { + Spacer() + Image( + DesignKitAsset.Images.personTalking.name, + bundle: Bundle(for: DesignKitResources.self) + ) + .resizable() + .renderingMode(.template) + .aspectRatio(contentMode: .fit) + .foregroundColor( + self.activityVM.isSpeaking + ? DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor + : DesignKitAsset.Colors.progressBar.swiftUIColor + ) + .padding(10) + } + ) + .clipShape(RoundedRectangle(cornerRadius: self.gameMetrics.roundedCorner, style: .circular)) + .shadow( + color: DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor.opacity(0.2), + radius: self.activityVM.isSpeaking ? 0 : 4, x: 0, y: self.activityVM.isSpeaking ? 1 : 4 + ) + .scaleEffect(self.activityVM.isSpeaking ? 0.98 : 1) + .onTapGesture { + self.activityVM.speak(sentence: self.activityVM.steps[self.activityVM.currentStep].instruction.localized()) + } + .disabled(self.activityVM.isSpeaking) + .animation(.easeOut(duration: 0.2), value: self.activityVM.isSpeaking) + } +} + +// MARK: - InstructionButton_Previews + +struct InstructionButton_Previews: PreviewProvider { + static var previews: some View { + InstructionButtonDeprecated(gameMetrics: GameMetrics()) + .environmentObject(ActivityViewModelDeprecated()) + .environmentObject(GameMetrics()) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/Components/LottieView.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/Components/LottieView.swift new file mode 100644 index 0000000000..5c5b295988 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/Components/LottieView.swift @@ -0,0 +1,100 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Lottie +import SwiftUI + +struct LottieView: UIViewRepresentable { + // MARK: Lifecycle + + public init( + name: String, + speed: CGFloat = 1, + reverse: Bool = false, + action: @escaping () -> Void = { /* default empty closure */ }, + play: Binding = .constant(true) + ) { + self.name = name + self.speed = speed + self.reverse = reverse + self.action = action + _play = play + } + + // MARK: Internal + + typealias UIViewType = UIView + + class Coordinator: NSObject { + // MARK: Lifecycle + + init(_ animationView: LottieView) { + self.parent = animationView + super.init() + } + + // MARK: Internal + + var parent: LottieView + } + + var name: String! + var speed: CGFloat + var reverse: Bool + var action: () -> Void + @Binding var play: Bool + + var animationView = LottieAnimationView() + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIView(context _: UIViewRepresentableContext) -> UIView { + let view = UIView() + + self.animationView.animation = LottieAnimation.named(self.name) + self.animationView.contentMode = .scaleAspectFit + self.animationView.animationSpeed = self.speed + self.animationView.loopMode = .playOnce + self.animationView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(self.animationView) + + NSLayoutConstraint.activate([ + self.animationView.widthAnchor.constraint(equalTo: view.widthAnchor), + self.animationView.heightAnchor.constraint(equalTo: view.heightAnchor), + ]) + + return view + } + + func updateUIView(_: UIView, context: UIViewRepresentableContext) { + if self.play { + if self.reverse { + context.coordinator.parent.animationView.play(fromProgress: 0.0, toProgress: 1.0, loopMode: .none) { + _ in + self.animationView.pause() + } + } else { + context.coordinator.parent.animationView.play { finished in + if finished { + self.animationView.pause() + self.action() + } + } + } + } else { + if self.reverse { + context.coordinator.parent.animationView.animationSpeed = self.speed * 1.5 + context.coordinator.parent.animationView.play(fromProgress: 1.0, toProgress: 0.0, loopMode: .none) { + _ in + context.coordinator.parent.animationView.stop() + context.coordinator.parent.animationView.animationSpeed = self.speed + } + } else { + context.coordinator.parent.animationView.stop() + } + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/Components/PlaySoundButton+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/Components/PlaySoundButton+DEPRECATED.swift new file mode 100644 index 0000000000..e6e80b008f --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/Components/PlaySoundButton+DEPRECATED.swift @@ -0,0 +1,48 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - PlaySoundButtonDeprecated + +struct PlaySoundButtonDeprecated: View { + @EnvironmentObject var activityVM: ActivityViewModelDeprecated + + var body: some View { + Button( + action: { + self.activityVM.audioPlayer.play() + }, + label: { + Image(systemName: "speaker.2") + .font(.system(size: 100, weight: .medium)) + .foregroundColor(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) + .padding(40) + } + ) + .buttonStyle(PlaySound_ButtonStyleDeprecated(progress: self.activityVM.progress)) + .frame( + width: 160, + height: 200, + alignment: .center + ) + .scaleEffect(self.activityVM.audioPlayer.isPlaying ? 1.0 : 0.8, anchor: .center) + .shadow( + color: DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor.opacity(0.2), + radius: self.activityVM.audioPlayer.isPlaying ? 6 : 3, x: 0, y: 3 + ) + .animation(.spring(response: 0.3, dampingFraction: 0.45), value: self.activityVM.audioPlayer.isPlaying) + .disabled(self.activityVM.audioPlayer.isPlaying) + } +} + +// MARK: - PlaySoundButton_Previews + +struct PlaySoundButton_Previews: PreviewProvider { + static var previews: some View { + PlaySoundButtonDeprecated() + .environmentObject(ActivityViewModelDeprecated()) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/Components/PlayZone+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/Components/PlayZone+DEPRECATED.swift new file mode 100644 index 0000000000..fb5a88a5be --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/Components/PlayZone+DEPRECATED.swift @@ -0,0 +1,128 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AVFoundation +import SwiftUI + +// MARK: - PlayZoneDeprecated + +struct PlayZoneDeprecated: View { + // MARK: Internal + + @ObservedObject var gameMetrics: GameMetrics + @EnvironmentObject var activityVM: ActivityViewModelDeprecated + + var body: some View { + HStack { + Spacer() + LazyVGrid(columns: self.columns, spacing: self.gameMetrics.playGridRowSpacing) { + ForEach(0.. some View { + Circle() + .fillDeprecated( + color, + strokeBorder: .white, + lineWidth: self.gameMetrics.stepMarkerBorderWidth + ) + .background(Circle().fill(.white)) + .padding(self.gameMetrics.stepMarkerPadding) + } +} + +// MARK: - ProgressBarView_Previews + +struct ProgressBarView_Previews: PreviewProvider { + static var previews: some View { + ProgressBarViewDeprecated(gameMetrics: GameMetrics()) + .environmentObject(ActivityViewModelDeprecated()) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/GameMetrics.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/GameMetrics.swift new file mode 100644 index 0000000000..59dd922fb5 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/GameMetrics.swift @@ -0,0 +1,50 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import SwiftUI + +class GameMetrics: NSObject, ObservableObject { + // MARK: - Views Metrics & Animations parameters + + // GameView + @Published var headerTotalHeight: CGFloat = 155 + @Published var headerSpacing: CGFloat = 40 + @Published var headerPadding: CGFloat = 30 + @Published var instructionFontSize: CGFloat = 22 + @Published var instructionFontWeight: Font.Weight = .regular + @Published var instructionFrame: CGSize = .init(width: 640, height: 85) + @Published var reg17: Font = .system(size: 17, weight: .regular) + @Published var semi17: Font = .system(size: 17, weight: .semibold) + + // Lottie Screens + @Published var motivatorScale: CGFloat = 1.2 + @Published var endAnimTextsSpacing: CGFloat = 40 + @Published var endAnimFontSize: CGFloat = 30 + @Published var endAnimFontWeight: Font.Weight = .black + @Published var endAnimFontDesign: Font.Design = .rounded + @Published var endAnimDuration: Double = 0.6 + @Published var endAnimDelayTop: Double = 1.2 + @Published var endAnimDelayBottom: Double = 1.0 + @Published var endAnimBtnDuration: Double = 0.25 + @Published var endAnimGameOverBtnDelay: Double = 1.6 + @Published var endAnimReplayBtnDelay: Double = 1.7 + @Published var endAnimBtnPadding: CGFloat = 80 + + // ProgressView + @Published var stepMarkerBorderWidth: CGFloat = 3 + // @Published var stepMarkerDiameter: CGFloat = 18 + @Published var stepMarkerPadding: CGFloat = 6 + @Published var progressViewHeight: CGFloat = 30 + + // PlayZone + @Published var playGridBtnCellSize: CGFloat = 250 + @Published var playGridBtnSize: CGFloat = 200 + @Published var playGridRowSpacing: CGFloat = 26 + @Published var playGridBtnTrimLineWidth: CGFloat = 6 + @Published var playGridBtnAnimDuration: Double = 0.3 + + // Misc + @Published var roundedCorner: CGFloat = 10 +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/GameView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/GameView+DEPRECATED.swift new file mode 100644 index 0000000000..dbe212a8d7 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Game/GameView+DEPRECATED.swift @@ -0,0 +1,257 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import Lottie +import SwiftUI + +struct GameViewDeprecated: View { + // MARK: Internal + + @StateObject var gameMetrics = GameMetrics() + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + @EnvironmentObject var robotVM: RobotViewModel + @EnvironmentObject var activityVM: ActivityViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + @Environment(\.dismiss) var dismiss + + var body: some View { + DesignKitAsset.Colors.lekaLightBlue.swiftUIColor + .edgesIgnoringSafeArea(.all) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden() + .overlay( + VStack(spacing: 0) { + VStack(spacing: self.gameMetrics.headerSpacing) { + ProgressBarViewDeprecated(gameMetrics: self.gameMetrics) + InstructionButtonDeprecated(gameMetrics: self.gameMetrics) + } + .frame(maxHeight: self.gameMetrics.headerTotalHeight) + .padding(.top, self.gameMetrics.headerPadding) + + VStack { + Spacer() + PlayZoneDeprecated(gameMetrics: self.gameMetrics) + .layoutPriority(1) + Spacer() + } + } + ) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button( + action: { + self.activityVM.resetActivity() + self.navigationVM.showActivitiesFullScreenCover = false + self.robotVM.userChoseToPlayWithoutRobot = false + // show alert to inform about losing the progress?? + }, + label: { + Image(systemName: "chevron.left") + .padding(.horizontal) + } + ) + .disabled(self.activityVM.tapIsDisabled) + } + ToolbarItem(placement: .principal) { + HStack(spacing: 4) { + Text(self.activityVM.currentActivityTitle) + if self.settings.companyIsConnected, self.settings.exploratoryModeIsOn { + Image(systemName: "binoculars.fill") + } + } + .font(.subheadline) + } + ToolbarItem(placement: .navigationBarTrailing) { + self.infoButton + } + } + .sheet(isPresented: self.$showInstructionModal) { + CurrentGameInstructionViewDeprecated(gameMetrics: self.gameMetrics) + } + .overlay( + ZStack { + if self.activityVM.showMotivator { + self.successScreen + } else if self.activityVM.showEndAnimation { + self.cheerScreen + } else { + // Not an EmptyView() to avoid breaking the opacity animation + Rectangle() + .fill(Color.clear) + } + } + .background(content: { + Group { + if self.activityVM.showBlurryBG { + Rectangle() + .fill(.regularMaterial) + .transition(.opacity) + } + } + .animation(.default, value: self.activityVM.showBlurryBG) + }) + .edgesIgnoringSafeArea(.all) + ) + .background( + GeometryReader { reader in + Color.clear + .onAppear { + self.offsetGameOverBtn = -reader.frame(in: .local).width / 2 + self.offsetReplayBtn = reader.frame(in: .local).width / 2 + self.initialBtnOffsets = [self.offsetGameOverBtn, self.offsetReplayBtn] + } + }) + } + + @ViewBuilder + func resultMessage(result: ResultType) -> some View { + // Localized Strings + let topMessage = switch result { + case .fail: + Text("fail_top_message") + case .medium: + Text("medium_top_message") + case .success: + Text("success_top_message") + default: + Text("") + } + + let bottomMessage = switch result { + case .fail: + Text("fail_bottom_message") + + Text("(0%)") + .foregroundColor(DesignKitAsset.Colors.bravoHighlights.swiftUIColor) + + Text(".") + case .medium, + .success: + Text("success_bottom_message") + + Text( + "success_bottom_result \(self.activityVM.goodAnswers) \(self.activityVM.numberOfSteps) \(self.activityVM.percentOfSuccess)" + ) + .foregroundColor(DesignKitAsset.Colors.bravoHighlights.swiftUIColor) + + Text("!") + default: + Text("") + } + + VStack(spacing: self.gameMetrics.endAnimTextsSpacing) { + Group { + topMessage + .foregroundColor(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) + .offset(y: self.textOffset) + .opacity(self.textOpacity) + .animation( + .easeOut(duration: self.gameMetrics.endAnimDuration).delay(self.gameMetrics.endAnimDelayTop), + value: self.textOffset + ) + .animation( + .easeOut(duration: self.gameMetrics.endAnimDuration).delay(self.gameMetrics.endAnimDelayTop), + value: self.textOpacity + ) + bottomMessage + .foregroundColor(DesignKitAsset.Colors.lekaDarkGray.swiftUIColor) + .offset(y: self.textOffset) + .opacity(self.textOpacity) + .animation( + .easeOut(duration: self.gameMetrics.endAnimDuration).delay(self.gameMetrics.endAnimDelayBottom), + value: self.textOffset + ) + .animation( + .easeOut(duration: self.gameMetrics.endAnimDuration).delay(self.gameMetrics.endAnimDelayBottom), + value: self.textOpacity + ) + } + .font(.largeTitle) + } + } + + // MARK: Private + + @State private var textOffset: CGFloat = -100 + @State private var textOpacity: Double = 0 + @State private var offsetGameOverBtn: CGFloat = 0 + @State private var offsetReplayBtn: CGFloat = 0 + @State private var initialBtnOffsets: [CGFloat] = [] + + @State private var showInstructionModal: Bool = false + + private var infoButton: some View { + Button { + self.showInstructionModal.toggle() + } label: { + Image(systemName: "info.circle") + .foregroundColor(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) + } + .opacity(self.showInstructionModal ? 0 : 1) + } + + private var successScreen: some View { + LottieView( + name: "motivator", speed: 0.5, + action: { self.activityVM.hideMotivator() }, + play: self.$activityVM.showMotivator + ) + .scaleEffect(self.gameMetrics.motivatorScale, anchor: .center) + } + + private var cheerScreen: some View { + ZStack { + LottieView( + name: self.activityVM.percentOfSuccess >= 80 ? "bravo" : "tryAgain", play: self.$activityVM.showEndAnimation + ) + .onAppear { + // Delayed to avoid artifacts on animation... SwiftUI bug + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.textOffset = 0 + self.textOpacity = 1 + self.offsetGameOverBtn = 0 + self.offsetReplayBtn = 0 + } + } + + VStack(spacing: 0) { + self.resultMessage(result: self.activityVM.result) + Spacer() + HStack { + Spacer() + Button { + self.navigationVM.showActivitiesFullScreenCover = false + self.robotVM.userChoseToPlayWithoutRobot = false + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + self.activityVM.resetActivity() + } + } label: { + Text("gameOver_button") + } + .buttonStyle(BorderedCapsule_ButtonStyleDeprecated(isFilled: false)) + .offset(x: self.offsetGameOverBtn) + .animation( + .easeOut(duration: self.gameMetrics.endAnimBtnDuration).delay(self.gameMetrics.endAnimGameOverBtnDelay), + value: self.offsetGameOverBtn + ) + Spacer() + Button { + self.activityVM.replayCurrentActivity() + self.textOffset = -100 + self.textOpacity = 0 + self.offsetGameOverBtn = self.initialBtnOffsets[0] + self.offsetReplayBtn = self.initialBtnOffsets[1] + } label: { + Text("replay_button") + } + .buttonStyle(BorderedCapsule_ButtonStyleDeprecated()) + .offset(x: self.offsetReplayBtn) + .animation( + .easeOut(duration: self.gameMetrics.endAnimBtnDuration).delay(self.gameMetrics.endAnimReplayBtnDelay), + value: self.offsetReplayBtn + ) + Spacer() + } + } + .padding(.vertical, self.gameMetrics.endAnimBtnPadding) + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/GlobalComponentViews/CloudsBGView.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/GlobalComponentViews/CloudsBGView.swift new file mode 100644 index 0000000000..b41644eb60 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/GlobalComponentViews/CloudsBGView.swift @@ -0,0 +1,26 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - CloudsBGView + +struct CloudsBGView: View { + var body: some View { + Image(uiImage: DesignKitAsset.Images.interfaceCloud.image) + .resizable() + .renderingMode(.original) + .aspectRatio(contentMode: .fill) + .edgesIgnoringSafeArea(.all) + } +} + +// MARK: - CloudsBGView_Previews + +struct CloudsBGView_Previews: PreviewProvider { + static var previews: some View { + CloudsBGView() + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/GlobalComponentViews/InformationTiles/InfoTile+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/GlobalComponentViews/InformationTiles/InfoTile+DEPRECATED.swift new file mode 100644 index 0000000000..a4de769160 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/GlobalComponentViews/InformationTiles/InfoTile+DEPRECATED.swift @@ -0,0 +1,126 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - InfoTileDeprecated + +struct InfoTileDeprecated: View { + // MARK: Internal + + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + @EnvironmentObject var viewRouter: ViewRouterDeprecated + @EnvironmentObject var metrics: UIMetrics + + let data: TileData + + var body: some View { + VStack(spacing: 0) { + self.tileHeader + self.tileContent + Spacer() + } + .frame(height: 266) + .background(.white) + .clipShape(RoundedRectangle(cornerRadius: self.metrics.tilesRadius, style: .continuous)) + } + + // MARK: Private + + private var headerColor: Color { + self.data == .discovery + ? DesignKitAsset.Colors.lekaOrange.swiftUIColor : DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor + } + + private var tileHeader: some View { + HStack { + switch self.data { + case .discovery, + .curriculums, + .activities, + .commands: + Image(systemName: self.data.content.image!) + default: + Image(self.data.content.image!) + .resizable() + .renderingMode(.template) + .aspectRatio(contentMode: .fit) + } + Spacer() + Text(self.data.content.title!) + .font(.title3) + Spacer() + if self.data != .discovery, self.settings.companyIsConnected { + self.closeButton + } + } + .padding(.vertical, 6) + .padding(.horizontal, 20) + .frame(height: 44) + .foregroundColor(.white) + .background(self.headerColor) + } + + private var tileContent: some View { + VStack { + Spacer() + Text(self.data.content.subtitle!) + .font(.headline) + .foregroundColor(self.headerColor) + .padding(10) + Spacer() + Text(self.data.content.message!) + .font(.subheadline) + .padding(10) + Spacer() + if self.data == .discovery { + self.connectButton + } + } + .multilineTextAlignment(.center) + .frame(maxWidth: 700) + } + + private var closeButton: some View { + Button { + self.navigationVM.updateShowInfo() + } label: { + Image(systemName: "multiply") + } + } + + private var connectButton: some View { + Button { + self.viewRouter.currentPage = .welcome + } label: { + Text(self.data.content.callToActionLabel!) + } + .padding(.horizontal, 20) + .buttonStyle( + BorderedCapsule_NoFeedback_ButtonStyleDeprecated( + font: .body, + color: DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor, + width: 300 + ) + ) + } +} + +// MARK: - InfoTile_Previews + +struct InfoTile_Previews: PreviewProvider { + static var previews: some View { + ZStack { + Color.teal.ignoresSafeArea() + InfoTileDeprecated(data: .discovery) + .environmentObject(SettingsViewModelDeprecated()) + .environmentObject(NavigationViewModelDeprecated()) + .environmentObject(UIMetrics()) + .environmentObject(ViewRouterDeprecated()) + .padding() + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/GlobalComponentViews/InformationTiles/InfoTileManager+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/GlobalComponentViews/InformationTiles/InfoTileManager+DEPRECATED.swift new file mode 100644 index 0000000000..cf46094817 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/GlobalComponentViews/InformationTiles/InfoTileManager+DEPRECATED.swift @@ -0,0 +1,32 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct InfoTileManagerDeprecated: View { + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + + var body: some View { + Group { + if !self.settings.companyIsConnected { + HStack(spacing: 15) { + InfoTileDeprecated(data: .discovery) + InfoTileDeprecated(data: self.navigationVM.contextualInfo()) + } + .padding(.horizontal, 20) + .padding(.vertical, 10) + } else if self.navigationVM.showInfo() { + InfoTileDeprecated(data: self.navigationVM.contextualInfo()) + .transition(.move(edge: .top).combined(with: .opacity)) + .padding(.horizontal, 20) + .padding(.vertical, 10) + } else { + EmptyView() + } + } + .animation(.easeOut(duration: 0.4), value: self.navigationVM.showInfo()) + .shadow(color: .black.opacity(0.1), radius: 3, x: 0, y: 2) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/GlobalComponentViews/LekaTextField+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/GlobalComponentViews/LekaTextField+DEPRECATED.swift new file mode 100644 index 0000000000..9e6d3e0f1b --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/GlobalComponentViews/LekaTextField+DEPRECATED.swift @@ -0,0 +1,118 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - LekaTextFieldDeprecated + +struct LekaTextFieldDeprecated: View { + // MARK: Internal + + @EnvironmentObject var metrics: UIMetrics + + var label: String + @Binding var entry: String + var color: Color = DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor + @Binding var isEditing: Bool + var type = FormField.mail + @FocusState var focused: FormField? + let action: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text(self.label) + .font(.body) + .foregroundColor(self.color) + .padding(.leading, 10) + self.entryField + } + } + + // MARK: Private + + @ViewBuilder + private var entryField: some View { + TextField("", text: self.$entry) { isEditingNow in + withAnimation { + self.isEditing = isEditingNow + } + } + .keyboardType(self.type == .mail ? .emailAddress : .default) + .textContentType(self.type == .mail ? .emailAddress : .name) + .submitLabel(self.type == .mail ? .next : .done) + .onSubmit { self.action() } + .autocapitalization(.none) + .autocorrectionDisabled() + .padding(10) + .frame(width: 400, height: 44) + .background( + DesignKitAsset.Colors.lekaLightGray.swiftUIColor, in: RoundedRectangle(cornerRadius: self.metrics.btnRadius) + ) + .overlay( + RoundedRectangle(cornerRadius: self.metrics.btnRadius) + .stroke(self.focused == self.type ? self.color : .clear, lineWidth: 1) + ) + } +} + +// MARK: - LekaPasswordFieldDeprecated + +struct LekaPasswordFieldDeprecated: View { + @EnvironmentObject var metrics: UIMetrics + + var label: String + @Binding var entry: String + var color: Color = DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor + + var type = FormField.password + @FocusState var focused: FormField? + @State private var isSecured: Bool = true + let action: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text(self.label) + .font(.body) + .foregroundColor(self.color) + .padding(.leading, 10) + self.passwordField + } + } + + @ViewBuilder + private var passwordField: some View { + HStack { + Group { + if self.isSecured { + SecureField("", text: self.$entry) + } else { + TextField("", text: self.$entry) + } + } + .padding(10) + .autocapitalization(.none) + .textContentType(.password) + .submitLabel(.continue) + .onSubmit { self.action() } + .focused(self.$focused, equals: self.type) + Spacer() + Button { + self.isSecured.toggle() + } label: { + Image(systemName: self.isSecured ? "eye" : "eye.slash") + .padding(10) + } + .disabled(self.entry.isEmpty) + } + .frame(width: 400, height: 44) + .background( + DesignKitAsset.Colors.lekaLightGray.swiftUIColor, in: RoundedRectangle(cornerRadius: self.metrics.btnRadius) + ) + .overlay( + RoundedRectangle(cornerRadius: self.metrics.btnRadius) + .stroke(self.focused == self.type ? self.color : .clear, lineWidth: 1) + ) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/SignUpPath/Shared/SignupNavigationTitle+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/SignUpPath/Shared/SignupNavigationTitle+DEPRECATED.swift new file mode 100644 index 0000000000..afbbffa57e --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/SignUpPath/Shared/SignupNavigationTitle+DEPRECATED.swift @@ -0,0 +1,15 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct SignupNavigationTitleDeprecated: View { + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + Text("Première connexion") + .font(.headline) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/SignUpPath/SignupFinalStep+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/SignUpPath/SignupFinalStep+DEPRECATED.swift new file mode 100644 index 0000000000..7fccf53844 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/SignUpPath/SignupFinalStep+DEPRECATED.swift @@ -0,0 +1,85 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct SignupFinalStepDeprecated: View { + // MARK: Internal + + @EnvironmentObject var metrics: UIMetrics + @EnvironmentObject var viewRouter: ViewRouterDeprecated + + var body: some View { + ZStack { + DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea() + self.tile + } + .edgesIgnoringSafeArea(.top) + .toolbar { + ToolbarItem(placement: .principal) { + SignupNavigationTitleDeprecated() + } + } + } + + // MARK: Private + + private let data: TileData = .signupFinalStep + + private var tile: some View { + HStack(alignment: .center, spacing: 0) { + VStack(spacing: 0) { + // Title + Text(self.data.content.title!) + .font(.headline) + .foregroundColor(DesignKitAsset.Colors.lekaOrange.swiftUIColor) + Spacer() + // Message + VStack(spacing: 10) { + Text(self.data.content.message!) + .padding(.bottom, 10) + VStack(alignment: .leading, spacing: 8) { + Text("✅ Créer votre profil de professionnel") + Text("✅ Créer votre 1er profil de personne accompagnée") + Text("Vous allez maintenant pouvoir découvrir l'univers Leka et le contenu éducatif.") + .padding(.vertical, 10) + } + } + .multilineTextAlignment(.center) + .font(.body) + Spacer() + // CTA Button + self.accessoryView + } + .multilineTextAlignment(.center) + .frame(width: 400) + .padding(self.metrics.tileContentPadding) + } + .frame( + width: self.metrics.tileSize.width, + height: self.metrics.tileSize.height + ) + .background( + .white, + in: RoundedRectangle(cornerRadius: self.metrics.tilesRadius, style: .continuous) + ) + } + + private var accessoryView: some View { + Button { + withAnimation { + self.viewRouter.currentPage = .home + } + } label: { + Text(self.data.content.callToActionLabel!) + } + .buttonStyle( + BorderedCapsule_NoFeedback_ButtonStyleDeprecated( + font: .body, + color: DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor + ) + ) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/SignUpPath/SignupStep1+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/SignUpPath/SignupStep1+DEPRECATED.swift new file mode 100644 index 0000000000..85e71aafad --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/SignUpPath/SignupStep1+DEPRECATED.swift @@ -0,0 +1,90 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct SignupStep1Deprecated: View { + // MARK: Internal + + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + ZStack { + DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea() + self.tile + } + .edgesIgnoringSafeArea(.top) + .navigationDestination(isPresented: self.$navigateToSignup2) { + SignupStep2Deprecated() + } + .toolbar { + ToolbarItem(placement: .principal) { + SignupNavigationTitleDeprecated() + } + } + } + + // MARK: Private + + private let data: TileData = .signupBravo + @State private var navigateToSignup2: Bool = false + + private var tile: some View { + HStack(alignment: .center, spacing: 0) { + VStack(spacing: 0) { + // Picto + Image( + self.data.content.image!, + bundle: Bundle(for: DesignKitResources.self) + ) + .resizable() + .renderingMode(.original) + .aspectRatio(contentMode: .fit) + .frame(height: self.metrics.tilePictoHeightSmall) + .padding(.bottom, 30) + // Title + Text(self.data.content.title!) + .font(.headline) + .foregroundColor(DesignKitAsset.Colors.lekaOrange.swiftUIColor) + Spacer() + // Message + Text(self.data.content.message!) + .font(.body) + Spacer() + // CTA Button + self.accessoryView + } + .multilineTextAlignment(.center) + .frame(width: self.metrics.tileContentWidth) + .padding(self.metrics.tileContentPadding) + } + .frame( + width: self.metrics.tileSize.width, + height: self.metrics.tileSize.height + ) + .background( + .white, + in: RoundedRectangle(cornerRadius: self.metrics.tilesRadius, style: .continuous) + ) + } + + private var accessoryView: some View { + Button( + action: { + self.navigateToSignup2.toggle() + }, + label: { + Text(self.data.content.callToActionLabel!) + } + ) + .buttonStyle( + BorderedCapsule_NoFeedback_ButtonStyleDeprecated( + font: .body, + color: DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor, + width: self.metrics.tileBtnWidth + ) + ) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/SignUpPath/SignupStep2+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/SignUpPath/SignupStep2+DEPRECATED.swift new file mode 100644 index 0000000000..40079210f3 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/SignUpPath/SignupStep2+DEPRECATED.swift @@ -0,0 +1,91 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct SignupStep2Deprecated: View { + // MARK: Internal + + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + ZStack { + DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea() + self.tile + } + .edgesIgnoringSafeArea(.top) + .navigationDestination(isPresented: self.$navigateToTeacherCreation) { + CreateTeacherProfileViewDeprecated() + } + .toolbar { + ToolbarItem(placement: .principal) { + SignupNavigationTitleDeprecated() + } + } + } + + // MARK: Private + + private let data: TileData = .signupStep1 + @State private var navigateToTeacherCreation: Bool = false + + private var tile: some View { + HStack(alignment: .center, spacing: 0) { + VStack(spacing: 0) { + // Picto + Image( + self.data.content.image!, + bundle: Bundle(for: DesignKitResources.self) + ) + .resizable() + .renderingMode(.original) + .aspectRatio(contentMode: .fit) + .frame(height: self.metrics.tilePictoHeightMedium) + .padding(.top, 20) + Spacer() + // Title + Text(self.data.content.title!) + .font(.headline) + .foregroundColor(DesignKitAsset.Colors.lekaOrange.swiftUIColor) + .padding(.vertical, 20) + // Message + Text(self.data.content.message!) + .font(.body) + Spacer() + // CTA Button + self.accessoryView + } + .multilineTextAlignment(.center) + .frame(width: self.metrics.tileContentWidth) + .padding(.bottom, self.metrics.tileContentPadding) + } + .frame( + width: self.metrics.tileSize.width, + height: self.metrics.tileSize.height + ) + .background( + .white, + in: RoundedRectangle(cornerRadius: self.metrics.tilesRadius, style: .continuous) + ) + } + + private var accessoryView: some View { + Button( + action: { + self.navigateToTeacherCreation.toggle() + }, + label: { + Text(self.data.content.callToActionLabel!) + } + ) + .buttonStyle( + BorderedCapsule_NoFeedback_ButtonStyleDeprecated( + font: .body, + color: DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor, + width: self.metrics.tileBtnWidth + ) + ) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/SignUpPath/SignupStep3+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/SignUpPath/SignupStep3+DEPRECATED.swift new file mode 100644 index 0000000000..49a3c7b940 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/SignUpPath/SignupStep3+DEPRECATED.swift @@ -0,0 +1,91 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct SignupStep3Deprecated: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + ZStack { + DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea() + self.tile + } + .edgesIgnoringSafeArea(.top) + .navigationDestination(isPresented: self.$navigateToUserCreation) { + CreateUserProfileViewDeprecated() + } + .toolbar { + ToolbarItem(placement: .principal) { + SignupNavigationTitleDeprecated() + } + } + } + + // MARK: Private + + private let data: TileData = .signupStep2 + @State private var navigateToUserCreation: Bool = false + + private var tile: some View { + HStack(alignment: .center, spacing: 0) { + VStack(spacing: 0) { + // Picto + Image( + self.data.content.image!, + bundle: Bundle(for: DesignKitResources.self) + ) + .resizable() + .renderingMode(.original) + .aspectRatio(contentMode: .fit) + .frame(height: self.metrics.tilePictoHeightMedium) + .padding(.vertical, 20) + // Title + Text(self.data.content.title!) + .font(.headline) + .foregroundColor(DesignKitAsset.Colors.lekaOrange.swiftUIColor) + Spacer() + // Message + Text(self.data.content.message!) + .font(.body) + Spacer() + // CTA Button + self.accessoryView + } + .multilineTextAlignment(.center) + .frame(width: self.metrics.tileContentWidth) + .padding(.bottom, self.metrics.tileContentPadding) + } + .frame( + width: self.metrics.tileSize.width, + height: self.metrics.tileSize.height + ) + .background( + .white, + in: RoundedRectangle(cornerRadius: self.metrics.tilesRadius, style: .continuous) + ) + } + + private var accessoryView: some View { + Button( + action: { + self.navigateToUserCreation.toggle() + }, + label: { + Text(self.data.content.callToActionLabel!) + } + ) + .buttonStyle( + BorderedCapsule_NoFeedback_ButtonStyleDeprecated( + font: .body, + color: DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor, + width: self.metrics.tileBtnWidth + ) + ) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/WelcomeViews/LoginView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/WelcomeViews/LoginView+DEPRECATED.swift new file mode 100644 index 0000000000..7438ceef1f --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/WelcomeViews/LoginView+DEPRECATED.swift @@ -0,0 +1,175 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - LoginViewDeprecated + +struct LoginViewDeprecated: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + @FocusState var focusedField: FormField? + + // TESTS ***************************************************************** + + var body: some View { + ZStack(alignment: .center) { + CloudsBGView() + + VStack(alignment: .center, spacing: 30) { + self.title + Group { + self.mailTextField + VStack(spacing: 0) { + self.passwordTextField + self.forgotLink + } + } + .frame(width: 400) + .disableAutocorrection(true) + .onAppear { self.focusedField = .mail } + self.submitButton + } + } + .navigationDestination(isPresented: self.$navigateToTeacherSelector) { + ProfileSelector_TeachersDeprecated() + } + } + + // Make sure you have set up Associated Domains for your app and AutoFill Passwords + // is enabled in Settings in order to get the strong password proposals etc... + // the same applies for both login/signup + // re-enable autofill modifiers in LekaTextField when OK (textContentType) + + func connectIsDisabled() -> Bool { + !self.mail.isValidEmailDeprecated() || self.mail.isEmpty || self.password.isEmpty + } + + // MARK: Private + + @State private var mail: String = "" + @State private var password: String = "" + @State private var isEditing = false + + @State private var navigateToTeacherSelector: Bool = false + + // TESTS ***************************************************************** + @State private var credentialsAreCorrect: Bool = true + + private var title: some View { + Text("Se connecter") + .textCase(.uppercase) + .font(.title) + } + + private var submitButton: some View { + Button( + action: { + self.submitForm() + }, + label: { + Text("Connexion") + .font(.body) + .padding(6) + .frame(width: 210) + } + ) + .disabled(self.connectIsDisabled()) + .buttonStyle(.borderedProminent) + .padding(.top, 24) + } + + @ViewBuilder + private var mailTextField: some View { + let mailTitle: String = { + guard self.mail.isValidEmailDeprecated() || self.mail.isEmpty || self.isEditing else { + return "Email incorrect" + } + return "Email" + }() + + let mailLabelColor: Color = self.mail.isValidEmailDeprecated() || self.mail.isEmpty || self.isEditing + ? DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor : .red + + LekaTextFieldDeprecated( + label: mailTitle, + entry: self.$mail, + color: mailLabelColor, + isEditing: self.$isEditing, + focused: _focusedField + ) { + self.focusedField = .password + } + } + + @ViewBuilder + private var passwordTextField: some View { + let passwordTitle: String = { + guard self.credentialsAreCorrect else { + return "Email ou mot de passe incorrect" + } + return "Mot de passe" + }() + + let passwordLabelColor: Color = self.credentialsAreCorrect ? DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor : .red + + LekaPasswordFieldDeprecated( + label: passwordTitle, + entry: self.$password, + color: passwordLabelColor, + focused: _focusedField + ) { + if self.password.isEmpty { + self.focusedField = .password + } else { + self.submitForm() + } + } + } + + private var forgotLink: some View { + HStack { + Spacer() + Link(destination: URL(string: "https://leka.io")!) { + Text("Mot de passe oublié") + .font(.footnote) + .underline() + .padding([.top, .trailing], 10) + } + } + } + + private func submitForm() { + if self.mail == LekaCompany().lekaCompany.mail { + if self.password != LekaCompany().lekaCompany.password { + self.credentialsAreCorrect = false + } else { + self.credentialsAreCorrect = true + self.settings.companyIsConnected = true + self.company.currentCompany = LekaCompany().lekaCompany + self.settings.companyIsLoggingIn = true + self.navigateToTeacherSelector.toggle() + } + } else { + self.credentialsAreCorrect = false + } + } +} + +// MARK: - LoginViewDeprecated_Previews + +struct LoginViewDeprecated_Previews: PreviewProvider { + static var previews: some View { + LoginViewDeprecated() + .environmentObject(CompanyViewModelDeprecated()) + .environmentObject(SettingsViewModelDeprecated()) + .environmentObject(UIMetrics()) + .previewInterfaceOrientation(.landscapeLeft) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/WelcomeViews/SignupView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/WelcomeViews/SignupView+DEPRECATED.swift new file mode 100644 index 0000000000..18f8d4045f --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/WelcomeViews/SignupView+DEPRECATED.swift @@ -0,0 +1,191 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - SignupViewDeprecated + +struct SignupViewDeprecated: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + @FocusState var focusedField: FormField? + + var body: some View { + ZStack(alignment: .center) { + CloudsBGView() + + VStack(alignment: .center, spacing: 30) { + self.title + Group { + self.mailTextField + self.passwordTextField + self.confirmTextField + } + .frame(width: 400) + .disableAutocorrection(true) + .onAppear { self.focusedField = .mail } + self.submitButton + } + } + .navigationDestination(isPresented: self.$navigateToSignup1) { + SignupStep1Deprecated() + } + } + + func connectIsDisabled() -> Bool { + !self.mail.isValidEmailDeprecated() || !self.passwordsMatch() || self.mail.isEmpty || self.password.isEmpty || self.confirm.isEmpty + || self.accountAlreadyExists + } + + func passwordsMatch() -> Bool { + self.password == self.confirm + } + + func checkAccountAvailability() { + self.accountAlreadyExists = (self.mail == "test@leka.io") + } + + // MARK: Private + + @State private var isEditing = false + @State private var mail: String = "" + @State private var password: String = "" + @State private var confirm: String = "" + @State private var accountAlreadyExists: Bool = false + @State private var navigateToSignup1: Bool = false + + @ViewBuilder + private var mailTextField: some View { + var mailTitle: String { + guard self.mail.isValidEmailDeprecated() || self.mail.isEmpty || self.isEditing else { + return "Email incorrect" + } + guard self.accountAlreadyExists else { + return "Email" + } + return "Ce compte existe déjà" + } + + var mailLabelColor: Color { + // TODO(@ladislas): review logic in the future + let color: Color = if self.mail.isValidEmailDeprecated() || self.mail.isEmpty || self.isEditing { + if self.accountAlreadyExists { + .red + } else { + DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor + } + } else { + .red + } + + return color + } + + LekaTextFieldDeprecated( + label: mailTitle, + entry: self.$mail, + color: mailLabelColor, + isEditing: self.$isEditing, + focused: _focusedField + ) { + self.focusedField = .password + } + .onChange(of: self.mail) { _ in + if self.accountAlreadyExists { + self.checkAccountAvailability() + } + } + } + + private var passwordTextField: some View { + LekaPasswordFieldDeprecated( + label: "Mot de passe", + entry: self.$password, + focused: _focusedField + ) { + if !self.password.isEmpty { + self.focusedField = .confirm + } else { + self.focusedField = .password + } + } + } + + @ViewBuilder + private var confirmTextField: some View { + let confirmTitle: String = { + guard self.passwordsMatch() || self.password.isEmpty || self.confirm.isEmpty else { + return "Les mots de passe ne sont pas identiques" + } + return "Confirmer le mot de passe" + }() + + let confirmLabelColor: Color = self.passwordsMatch() || self.confirm.isEmpty ? DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor : .red + + LekaPasswordFieldDeprecated( + label: confirmTitle, + entry: self.$confirm, + color: confirmLabelColor, + type: .confirm, + focused: _focusedField + ) { + if self.confirm.isEmpty || !self.passwordsMatch() { + self.focusedField = .confirm + } else { + self.submitForm() + } + } + } + + private var title: some View { + Text("Créer un compte") + .font(.title) + } + + private var submitButton: some View { + Button( + action: { + self.submitForm() + }, + label: { + Text("Connexion") + .font(.body) + .padding(6) + .frame(width: 210) + } + ) + .disabled(self.connectIsDisabled()) + .buttonStyle(.borderedProminent) + } + + private func submitForm() { + self.checkAccountAvailability() + if self.accountAlreadyExists { + self.password = "" + self.confirm = "" + } else { + self.company.currentCompany.mail = self.mail + self.company.currentCompany.password = self.password + self.settings.companyIsConnected = true + self.navigateToSignup1.toggle() + } + } +} + +// MARK: - SignupViewDeprecated_Previews + +struct SignupViewDeprecated_Previews: PreviewProvider { + static var previews: some View { + SignupViewDeprecated() + .environmentObject(CompanyViewModelDeprecated()) + .environmentObject(SettingsViewModelDeprecated()) + .environmentObject(UIMetrics()) + .previewInterfaceOrientation(.landscapeLeft) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/WelcomeViews/WelcomeView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/WelcomeViews/WelcomeView+DEPRECATED.swift new file mode 100644 index 0000000000..946673dea8 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Identification/WelcomeViews/WelcomeView+DEPRECATED.swift @@ -0,0 +1,85 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - WelcomeViewDeprecated + +struct WelcomeViewDeprecated: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var viewRouter: ViewRouterDeprecated + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + NavigationStack { + ZStack(alignment: .center) { + DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea() + + VStack(spacing: 30) { + self.logoLeka + + NavigationLink("Créer un compte") { + SignupViewDeprecated() + } + .buttonStyle(Connect_ButtonStyleDeprecated()) + + NavigationLink("Se connecter") { + LoginViewDeprecated() + } + .buttonStyle(Connect_ButtonStyleDeprecated(reversed: true)) + } + } + .edgesIgnoringSafeArea(.top) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + self.skipButton + } + } + } + } + + // MARK: Private + + private var logoLeka: some View { + Image( + DesignKitAsset.Assets.lekaLogo.name, + bundle: Bundle(for: DesignKitResources.self) + ) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 90) + .padding(.bottom, 30) + } + + private var skipButton: some View { + Button( + action: { + self.company.setupDiscoveryCompany() + self.viewRouter.currentPage = .home + }, + label: { + HStack(spacing: 4) { + Text("Passer cette étape") + Image(systemName: "chevron.right") + } + } + ) + } +} + +// MARK: - WelcomeViewDeprecated_Previews + +struct WelcomeViewDeprecated_Previews: PreviewProvider { + static var previews: some View { + WelcomeViewDeprecated() + .environmentObject(CompanyViewModelDeprecated()) + .environmentObject(ViewRouterDeprecated()) + .environmentObject(UIMetrics()) + .previewInterfaceOrientation(.landscapeLeft) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfileEditorView.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfileEditorView.swift new file mode 100644 index 0000000000..c715f78c5a --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfileEditorView.swift @@ -0,0 +1,104 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - ProfileEditorView + +struct ProfileEditorView: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + @Environment(\.dismiss) var dismiss + + var body: some View { + ZStack { + DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea() + + Group { + HStack(spacing: 40) { + Spacer() + ProfileSet_TeachersDeprecated() + Spacer() + ProfileSet_UsersDeprecated() + Spacer() + } + .padding(.top, 60) + } + } + .onAppear { + if !self.settings.companyIsConnected { + self.company.emptyProfilesSelection() + } + } + .toolbar { + ToolbarItem(placement: .principal) { self.navigationTitle } + ToolbarItem(placement: .navigationBarLeading) { self.closeButton } + ToolbarItem(placement: .navigationBarTrailing) { self.validateButton } + } + } + + // MARK: Private + + // Toolbar + private var navigationTitle: some View { + HStack(spacing: 4) { + Text("Choisir ou créer de nouveaux profils") + if self.settings.companyIsConnected, self.settings.exploratoryModeIsOn { + Image(systemName: "binoculars.fill") + } + } + .font(.headline) + } + + private var closeButton: some View { + Button( + action: { + // Leave without saving new selection + self.dismiss() + }, + label: { + Text("Fermer") + } + ) + } + + private var validateButton: some View { + Button { + if self.settings.companyIsConnected { + if self.settings.exploratoryModeIsOn { + self.settings.showSwitchOffExploratoryAlert.toggle() + } else { + // Save new selection and leave + self.company.assignCurrentProfiles() + self.dismiss() + } + } else { + self.settings.showConnectInvite.toggle() + } + } label: { + HStack(spacing: 4) { + Image(systemName: "checkmark.circle") + Text("Valider la sélection") + } + } + .disabled(!self.settings.companyIsConnected) + .disabled(!self.company.selectionSetIsCorrect()) + } +} + +// MARK: - ProfileEditorView_Previews + +struct ProfileEditorView_Previews: PreviewProvider { + static var previews: some View { + ProfileEditorView() + .environmentObject(CompanyViewModelDeprecated()) + .environmentObject(SettingsViewModelDeprecated()) + .environmentObject(UIMetrics()) + .previewInterfaceOrientation(.landscapeLeft) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfileSelectors/ProfileSelector_Teachers+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfileSelectors/ProfileSelector_Teachers+DEPRECATED.swift new file mode 100644 index 0000000000..463d37e903 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfileSelectors/ProfileSelector_Teachers+DEPRECATED.swift @@ -0,0 +1,32 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct ProfileSelector_TeachersDeprecated: View { + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + ZStack { + DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea() + + ProfileSet_TeachersDeprecated() + .padding(.top, 60) + } + .toolbar { + ToolbarItem(placement: .principal) { + HStack(spacing: 4) { + if self.settings.companyIsConnected, self.settings.exploratoryModeIsOn { + Image(systemName: "binoculars.fill") + } + Text("Choisir ou créer de nouveaux profils") + } + .font(.headline) + } + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfileSelectors/ProfileSelector_Users+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfileSelectors/ProfileSelector_Users+DEPRECATED.swift new file mode 100644 index 0000000000..746bc2f656 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfileSelectors/ProfileSelector_Users+DEPRECATED.swift @@ -0,0 +1,48 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct ProfileSelector_UsersDeprecated: View { + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + @Environment(\.dismiss) var dismiss + + var body: some View { + ZStack { + DesignKitAsset.Colors.lekaLightBlue.swiftUIColor.ignoresSafeArea() + + ProfileSet_UsersDeprecated() + .padding(.top, 60) + } + .navigationBarBackButtonHidden() + .toolbar { + ToolbarItem(placement: .principal) { + HStack(spacing: 4) { + if self.settings.companyIsConnected, self.settings.exploratoryModeIsOn { + Image(systemName: "binoculars.fill") + } + Text("Choisir ou créer de nouveaux profils") + } + .font(.headline) + } + ToolbarItem(placement: .navigationBarLeading) { + Button( + action: { + self.navigationVM.showActivitiesFullScreenCover = false + }, + label: { + HStack(spacing: 4) { + Image(systemName: "chevron.left") + Text("Retour") + } + } + ) + } + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/PickerTriggers/AvatarPickerTriggerButton_Teacher+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/PickerTriggers/AvatarPickerTriggerButton_Teacher+DEPRECATED.swift new file mode 100644 index 0000000000..5a58be11f1 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/PickerTriggers/AvatarPickerTriggerButton_Teacher+DEPRECATED.swift @@ -0,0 +1,26 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct AvatarPickerTriggerButton_TeachersDeprecated: View { + @EnvironmentObject var company: CompanyViewModelDeprecated + + @Binding var navigate: Bool + + var body: some View { + Button( + action: { + self.navigate.toggle() + }, + label: { + VStack(spacing: 10) { + AvatarTriggerImageViewDeprecated(img: self.company.getSelectedProfileAvatar(.teacher)) + AvatarTriggerCTAViewDeprecated() + } + } + ) + .buttonStyle(NoFeedback_ButtonStyleDeprecated()) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/PickerTriggers/AvatarPickerTriggerButton_Users+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/PickerTriggers/AvatarPickerTriggerButton_Users+DEPRECATED.swift new file mode 100644 index 0000000000..166846c89a --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/PickerTriggers/AvatarPickerTriggerButton_Users+DEPRECATED.swift @@ -0,0 +1,26 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct AvatarPickerTriggerButton_UsersDeprecated: View { + @EnvironmentObject var company: CompanyViewModelDeprecated + + @Binding var navigate: Bool + + var body: some View { + Button( + action: { + self.navigate.toggle() + }, + label: { + VStack(spacing: 10) { + AvatarTriggerImageViewDeprecated(img: self.company.getSelectedProfileAvatar(.user)) + AvatarTriggerCTAViewDeprecated() + } + } + ) + .buttonStyle(NoFeedback_ButtonStyleDeprecated()) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/PickerTriggers/AvatarPickerTriggersComponentViews+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/PickerTriggers/AvatarPickerTriggersComponentViews+DEPRECATED.swift new file mode 100644 index 0000000000..72f72f6402 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/PickerTriggers/AvatarPickerTriggersComponentViews+DEPRECATED.swift @@ -0,0 +1,51 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - AvatarTriggerImageViewDeprecated + +struct AvatarTriggerImageViewDeprecated: View { + var img: String + + var body: some View { + ZStack { + Circle() + .fill(.white) + .shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 4) + Group { + Image(self.img, bundle: Bundle(for: DesignKitResources.self)) + .resizable() + .aspectRatio(contentMode: .fit) + .clipShape(Circle()) + .padding(10) + } + .shadow(color: .black.opacity(0.2), radius: 3, x: 0, y: 2) + Circle() + .inset(by: 10) + .strokeBorder(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor, lineWidth: 4, antialiased: true) + } + .frame(width: 170, height: 170) + } +} + +// MARK: - AvatarTriggerCTAViewDeprecated + +struct AvatarTriggerCTAViewDeprecated: View { + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + Text("choisir un avatar") + .font(.body) + .padding(.vertical, 4) + .padding(.horizontal, 20) + .overlay( + Capsule() + .stroke(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor, lineWidth: 1) + ) + .background(.white, in: Capsule()) + .padding(10) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/AvatarPicker_Teachers+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/AvatarPicker_Teachers+DEPRECATED.swift new file mode 100644 index 0000000000..1fb5ac1245 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/AvatarPicker_Teachers+DEPRECATED.swift @@ -0,0 +1,45 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct AvatarPicker_TeachersDeprecated: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + + var body: some View { + ZStack { + Color.white.edgesIgnoringSafeArea(.top) + + AvatarPickerStoreDeprecated(selected: self.$selected) + .onAppear { + self.selected = self.company.bufferTeacher.avatar + } + ._safeAreaInsets(EdgeInsets(top: 40, leading: 0, bottom: 20, trailing: 0)) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .principal) { AvatarPicker_NavigationTitleDeprecated() } + ToolbarItem(placement: .navigationBarLeading) { AvatarPicker_AdaptiveBackButtonDeprecated() } + ToolbarItem(placement: .navigationBarTrailing) { + AvatarPicker_ValidateButtonDeprecated( + selected: self.$selected, + action: { + self.company.setBufferAvatar(self.selected, for: .teacher) + } + ) + } + } + } + .toolbarBackground(self.navigationVM.showProfileEditor ? .visible : .automatic, for: .navigationBar) + .preferredColorScheme(.light) + } + + // MARK: Private + + @State private var selected: String = "" +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/AvatarPicker_Users+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/AvatarPicker_Users+DEPRECATED.swift new file mode 100644 index 0000000000..eae05349ff --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/AvatarPicker_Users+DEPRECATED.swift @@ -0,0 +1,45 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct AvatarPicker_UsersDeprecated: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + + var body: some View { + ZStack { + Color.white.edgesIgnoringSafeArea(.top) + + AvatarPickerStoreDeprecated(selected: self.$selected) + .onAppear { + self.selected = self.company.bufferUser.avatar + } + ._safeAreaInsets(EdgeInsets(top: 40, leading: 0, bottom: 20, trailing: 0)) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .principal) { AvatarPicker_NavigationTitleDeprecated() } + ToolbarItem(placement: .navigationBarLeading) { AvatarPicker_AdaptiveBackButtonDeprecated() } + ToolbarItem(placement: .navigationBarTrailing) { + AvatarPicker_ValidateButtonDeprecated( + selected: self.$selected, + action: { + self.company.setBufferAvatar(self.selected, for: .user) + } + ) + } + } + } + .toolbarBackground(self.navigationVM.showProfileEditor ? .visible : .automatic, for: .navigationBar) + .preferredColorScheme(.light) + } + + // MARK: Private + + @State private var selected: String = "" +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/Components/AvatarButtonLabel+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/Components/AvatarButtonLabel+DEPRECATED.swift new file mode 100644 index 0000000000..2edbfb0f8e --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/Components/AvatarButtonLabel+DEPRECATED.swift @@ -0,0 +1,33 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// Avatar Buttons within the AvatarPicker() +struct AvatarButtonLabelDeprecated: View { + @EnvironmentObject var metrics: UIMetrics + + @Binding var image: String + @Binding var isSelected: Bool + + var body: some View { + ZStack { + Image(self.image, bundle: Bundle(for: DesignKitResources.self)) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: self.metrics.diameter, maxHeight: self.metrics.diameter) + .background(.white) + .mask(Circle()) + Circle() + .strokeBorder(DesignKitAsset.Colors.lekaLightGray.swiftUIColor, lineWidth: 2) + } + .frame(minWidth: self.metrics.diameter, maxWidth: self.metrics.diameter) + .background( + DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor, + in: Circle().inset(by: self.isSelected ? -7 : 2) + ) + .animation(.default, value: self.isSelected) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/Components/AvatarPickerStore+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/Components/AvatarPickerStore+DEPRECATED.swift new file mode 100644 index 0000000000..cee07a9769 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/Components/AvatarPickerStore+DEPRECATED.swift @@ -0,0 +1,56 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct AvatarPickerStoreDeprecated: View { + // MARK: Internal + + @EnvironmentObject var metrics: UIMetrics + + @Binding var selected: String + + var body: some View { + ScrollView(.vertical, showsIndicators: true) { + ForEach(AvatarSetsDeprecated.allCases, id: \.id) { category in + self.makeAvatarCategoryRow(category: category.content) + .id(category.id) + } + } + } + + // MARK: Private + + private func makeAvatarCategoryRow(category: AvatarCategoryDeprecated) -> some View { + VStack(alignment: .leading, spacing: 10) { + Text(category.category) + .font(.headline) + .padding(.leading, 40) + ScrollView(.horizontal, showsIndicators: true) { + LazyHGrid(rows: [GridItem()], spacing: 50) { + ForEach(category.images, id: \.self) { item in + Button { + if self.selected == item { + self.selected = "" + } else { + self.selected = item + } + } label: { + AvatarButtonLabelDeprecated( + image: .constant(item), + isSelected: .constant(self.selected == item) + ) + } + .buttonStyle(NoFeedback_ButtonStyleDeprecated()) + .id(item) + } + } + } + .frame(height: 179) + ._safeAreaInsets(EdgeInsets(top: 0, leading: 40, bottom: 0, trailing: 40)) + } + .padding(.bottom, 10) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/Components/AvatarPickers_NavBarComponents+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/Components/AvatarPickers_NavBarComponents+DEPRECATED.swift new file mode 100644 index 0000000000..f29993ad1b --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/AvatarPicker/Pickers/Components/AvatarPickers_NavBarComponents+DEPRECATED.swift @@ -0,0 +1,64 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - AvatarPicker_NavigationTitleDeprecated + +struct AvatarPicker_NavigationTitleDeprecated: View { + @EnvironmentObject var metrics: UIMetrics + @EnvironmentObject var company: CompanyViewModelDeprecated + + var body: some View { + Text("Quel est ton avatar ?") + .font(.headline) + } +} + +// MARK: - AvatarPicker_AdaptiveBackButtonDeprecated + +struct AvatarPicker_AdaptiveBackButtonDeprecated: View { + @EnvironmentObject var viewRouter: ViewRouterDeprecated + @Environment(\.dismiss) var dismiss + + var body: some View { + Button { + // go back without saving + self.dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "chevron.left") + if self.viewRouter.currentPage == .welcome { + Text("Retour") + } else { + Text("Annuler") + } + } + } + } +} + +// MARK: - AvatarPicker_ValidateButtonDeprecated + +struct AvatarPicker_ValidateButtonDeprecated: View { + @EnvironmentObject var company: CompanyViewModelDeprecated + @Environment(\.dismiss) var dismiss + + @Binding var selected: String + var action: () -> Void + + var body: some View { + Button { + self.action() + self.dismiss() + } label: { + HStack(spacing: 6) { + Image(systemName: "checkmark.circle") + Text("Valider la sélection") + } + } + .disabled(self.selected.isEmpty) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/CreateProfiles/Components/DeleteProfileButton+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/CreateProfiles/Components/DeleteProfileButton+DEPRECATED.swift new file mode 100644 index 0000000000..2eedeb29a6 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/CreateProfiles/Components/DeleteProfileButton+DEPRECATED.swift @@ -0,0 +1,27 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct DeleteProfileButtonDeprecated: View { + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + @Binding var show: Bool + + var body: some View { + if self.company.editingProfile { + Button { + self.show.toggle() + } label: { + Label("Supprimer le profil", systemImage: "trash.fill") + .padding(.horizontal, 20) + } + .buttonStyle(BorderedCapsule_NoFeedback_ButtonStyleDeprecated(font: .body, color: Color.red)) + .padding(.vertical, 10) + } else { + EmptyView() + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/CreateProfiles/Components/JobPickerTrigger+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/CreateProfiles/Components/JobPickerTrigger+DEPRECATED.swift new file mode 100644 index 0000000000..d2a06cd393 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/CreateProfiles/Components/JobPickerTrigger+DEPRECATED.swift @@ -0,0 +1,54 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct JobPickerTriggerDeprecated: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + @Binding var navigate: Bool + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text("Profession(s)") + .font(.body) + .padding(.leading, 10) + + Button { + self.navigate.toggle() + } label: { + self.buttonLabel + } + + if !self.company.bufferTeacher.jobs.isEmpty { + ForEach(self.company.bufferTeacher.jobs, id: \.self) { profession in + JobTagDeprecated(profession: profession) + } + } + } + .frame(width: 435) + } + + // MARK: Private + + private var buttonLabel: some View { + HStack(spacing: 0) { + Text("Sélectionnez") + .multilineTextAlignment(.leading) + .foregroundColor(.black) + .padding(10) + Spacer() + Image(systemName: "chevron.down") + .padding(10) + } + .frame(width: 400, height: 44) + .background( + DesignKitAsset.Colors.lekaLightGray.swiftUIColor, in: RoundedRectangle(cornerRadius: self.metrics.btnRadius) + ) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/CreateProfiles/Components/ReinforcerPicker+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/CreateProfiles/Components/ReinforcerPicker+DEPRECATED.swift new file mode 100644 index 0000000000..80febf4bef --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/CreateProfiles/Components/ReinforcerPicker+DEPRECATED.swift @@ -0,0 +1,78 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct ReinforcerPickerDeprecated: View { + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + if self.company.editingProfile { + VStack(spacing: 10) { + HStack { + Text("Choix du renforçateur") + .font(.body) + .padding(.leading, 10) + Spacer() + } + HStack { + Text( // swiftlint:disable:next line_length + "Le renforçateur est un effet lumineux répétitif du robot que vous pourrez actionner pour récompenser le comportement de l'utilisateur. \nSi votre robot est connecté, vous pouvez tester les renforçateurs avant d'en choisir un." + ) + .font(.footnote) + .padding(.leading, 10) + Spacer() + } + + // ReinforcerPicker + VStack(spacing: 4) { + HStack(spacing: 12) { + ForEach(1...3, id: \.self) { item in + self.reinforcerButton(item) + } + } + .padding(.horizontal, 10) + .frame(height: 114) + + HStack(spacing: 12) { + ForEach(4...5, id: \.self) { item in + self.reinforcerButton(item) + } + } + .padding(.horizontal, 10) + .frame(height: 114) + } + .frame(width: 410, height: 271, alignment: .center) + } + .frame(width: 420) + .animation(.default, value: self.company.bufferUser.reinforcer) + } else { + EmptyView() + } + } + + func reinforcerButton(_ number: Int) -> some View { + Button { + self.company.bufferUser.reinforcer = number + } label: { + Image("reinforcer-\(number)") + .resizable() + .renderingMode(.original) + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 108, maxHeight: 108) + .padding(10) + } + .background( + Circle() + .fill(.white) + .shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 4) + ) + .background( + DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor, + in: Circle().inset(by: self.company.bufferUser.reinforcer == number ? -6 : 2) + ) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/CreateProfiles/CreateTeacherProfileView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/CreateProfiles/CreateTeacherProfileView+DEPRECATED.swift new file mode 100644 index 0000000000..a5f9a6f748 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/CreateProfiles/CreateTeacherProfileView+DEPRECATED.swift @@ -0,0 +1,202 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - CreateTeacherProfileViewDeprecated + +struct CreateTeacherProfileViewDeprecated: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var viewRouter: ViewRouterDeprecated + @EnvironmentObject var metrics: UIMetrics + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + @Environment(\.dismiss) var dismiss + + var body: some View { + ZStack { + Color.white.edgesIgnoringSafeArea(.top) + + ScrollView(showsIndicators: false) { + VStack(spacing: 30) { + AvatarPickerTriggerButton_TeachersDeprecated(navigate: self.$navigateToAvatarPicker) + .padding(.top, 30) + + Group { + self.nameField + JobPickerTriggerDeprecated(navigate: self.$navigateToJobPicker) + } + self.accessoryView + Spacer() + DeleteProfileButtonDeprecated(show: self.$showDeleteConfirmation) + } + } + } + .interactiveDismissDisabled() + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbarBackground(self.navigationVM.showProfileEditor ? .visible : .automatic, for: .navigationBar) + .navigationDestination(isPresented: self.$navigateToAvatarPicker) { + AvatarPicker_TeachersDeprecated() + } + .navigationDestination(isPresented: self.$navigateToJobPicker) { + JobPickerDeprecated() + } + .navigationDestination(isPresented: self.$navigateToSignup3) { + SignupStep3Deprecated() + } + .alert("Supprimer le profil", isPresented: self.$showDeleteConfirmation) { + self.alertContent + } message: { + Text( + "Vous êtes sur le point de supprimer le profil accompagnant de \(self.company.bufferTeacher.name). \nCette action est irreversible." + ) + } + .toolbar { + ToolbarItem(placement: .principal) { self.navigationTitle } + ToolbarItem(placement: .navigationBarLeading) { self.adaptiveBackButton } + ToolbarItem(placement: .navigationBarTrailing) { self.validateButton } + } + .preferredColorScheme(.light) + } + + @ViewBuilder + var validateButton: some View { + if self.viewRouter.currentPage == .welcome { + EmptyView() + } else { + Button( + action: { + self.company.saveProfileChanges(.teacher) + if self.settings.companyIsLoggingIn { + self.company.assignCurrentProfiles() + self.viewRouter.currentPage = .home + self.settings.companyIsLoggingIn = false + } else { + self.dismiss() + } + hideKeyboard() + }, + label: { + self.validateButtonLabel + } + ) + .disabled(self.company.bufferTeacher.name.isEmpty) + } + } + + // MARK: Private + + @FocusState private var focusedField: FormField? + @State private var isEditing = false + @State private var showDeleteConfirmation: Bool = false + @State private var navigateToSignup3: Bool = false + @State private var navigateToJobPicker: Bool = false + @State private var navigateToAvatarPicker: Bool = false + + private var nameField: some View { + LekaTextFieldDeprecated( + label: "Nom d'accompagnant", entry: self.$company.bufferTeacher.name, isEditing: self.$isEditing, type: .name, + focused: _focusedField, + action: { + self.focusedField = nil + } + ) + .padding(2) + .onAppear { + self.focusedField = .name + } + } + + private var alertContent: some View { + Button(role: .destructive) { + self.company.deleteProfile(.teacher) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + self.dismiss() + } + } label: { + Text("Supprimer") + } + } + + @ViewBuilder + private var accessoryView: some View { + if self.viewRouter.currentPage == .welcome { + Button( + action: { + self.navigateToSignup3.toggle() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.company.addTeacherProfile() + self.company.assignCurrentProfiles() + } + }, + label: { + Text("Enregistrer ce profil") + } + ) + .disabled(self.company.bufferTeacher.name.isEmpty) + .buttonStyle( + BorderedCapsule_NoFeedback_ButtonStyleDeprecated( + font: .body, + color: DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor, + width: self.metrics.tileBtnWidth + ) + ) + } else { + EmptyView() + } + } + + // Toolbar + private var navigationTitle: some View { + Text(self.company.editingProfile ? "Éditer un profil accompagnant" : "Créer un profil accompagnant") + .font(.headline) + } + + private var validateButtonLabel: some View { + HStack(spacing: 4) { + Image(systemName: "checkmark.circle") + Text("Enregistrer") + } + + .contentShape(Rectangle()) + } + + private var adaptiveBackButton: some View { + Button { + // go back without saving + self.dismiss() + self.company.editingProfile = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.company.resetBufferProfile(.teacher) + } + } label: { + HStack(spacing: 4) { + Image(systemName: "chevron.left") + if self.navigationVM.showProfileEditor { + Text("Annuler") + } else { + Text("Retour") + } + } + } + } +} + +// MARK: - CreateProfileView_Previews + +struct CreateProfileView_Previews: PreviewProvider { + static var previews: some View { + CreateTeacherProfileViewDeprecated() + .environmentObject(CompanyViewModelDeprecated()) + .environmentObject(SettingsViewModelDeprecated()) + .environmentObject(NavigationViewModelDeprecated()) + .environmentObject(ViewRouterDeprecated()) + .environmentObject(UIMetrics()) + .previewInterfaceOrientation(.landscapeLeft) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/CreateProfiles/CreateUserProfileView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/CreateProfiles/CreateUserProfileView+DEPRECATED.swift new file mode 100644 index 0000000000..f1280f51a0 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/CreateProfiles/CreateUserProfileView+DEPRECATED.swift @@ -0,0 +1,202 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - CreateUserProfileViewDeprecated + +struct CreateUserProfileViewDeprecated: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var viewRouter: ViewRouterDeprecated + @EnvironmentObject var metrics: UIMetrics + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + @Environment(\.dismiss) var dismiss + + var body: some View { + ZStack { + Color.white.edgesIgnoringSafeArea(.top) + + ScrollView(showsIndicators: false) { + VStack(spacing: 30) { + AvatarPickerTriggerButton_UsersDeprecated(navigate: self.$navigateToAvatarPicker) + .padding(.top, 30) + + self.nameField + ReinforcerPickerDeprecated() + self.accessoryView + Spacer() + DeleteProfileButtonDeprecated(show: self.$showDeleteConfirmation) + } + } + } + .interactiveDismissDisabled() + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbarBackground(self.navigationVM.showProfileEditor ? .visible : .automatic, for: .navigationBar) + .navigationDestination(isPresented: self.$navigateToAvatarPicker) { + AvatarPicker_UsersDeprecated() + } + .navigationDestination(isPresented: self.$navigateToSignupFinalStep) { + SignupFinalStepDeprecated() + } + .alert("Supprimer le profil", isPresented: self.$showDeleteConfirmation) { + self.alertContent + } message: { + Text( + "Vous êtes sur le point de supprimer le profil utilisateur de \(self.company.bufferUser.name). \nCette action est irreversible." + ) + } + .toolbar { + ToolbarItem(placement: .principal) { self.navigationTitle } + ToolbarItem(placement: .navigationBarLeading) { self.adaptativeBackButton } + ToolbarItem(placement: .navigationBarTrailing) { self.validateButton } + } + .preferredColorScheme(.light) + } + + // MARK: Private + + @FocusState private var focusedField: FormField? + @State private var isEditing = false + @State private var showDeleteConfirmation: Bool = false + @State private var navigateToSignupFinalStep: Bool = false + @State private var navigateToAvatarPicker: Bool = false + + private var nameField: some View { + LekaTextFieldDeprecated( + label: "Nom d'utilisateur", entry: self.$company.bufferUser.name, isEditing: self.$isEditing, type: .name, + focused: _focusedField, + action: { + self.focusedField = nil + } + ) + .padding(2) + .onAppear { + self.focusedField = .name + } + } + + private var alertContent: some View { + Button(role: .destructive) { + self.company.deleteProfile(.user) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + self.dismiss() + } + } label: { + Text("Supprimer") + } + } + + @ViewBuilder + private var accessoryView: some View { + if self.viewRouter.currentPage == .welcome { + Button( + action: { + self.navigateToSignupFinalStep.toggle() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.company.addUserProfile() + self.company.assignCurrentProfiles() + } + }, + label: { + Text("Enregistrer ce profil") + } + ) + .disabled(self.company.bufferTeacher.name.isEmpty) + .buttonStyle( + BorderedCapsule_NoFeedback_ButtonStyleDeprecated( + font: .body, + color: DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor, + width: self.metrics.tileBtnWidth + ) + ) + } else { + EmptyView() + } + } + + // Toolbar + private var navigationTitle: some View { + Text(self.company.editingProfile ? "Éditer un profil utilisateur" : "Créer un profil utilisateur") + .font(.headline) + } + + private var adaptativeBackButton: some View { + Button { + // Leave without saving + self.dismiss() + self.company.editingProfile = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.company.resetBufferProfile(.user) + } + } label: { + HStack(spacing: 4) { + Image(systemName: "chevron.left") + if self.viewRouter.currentPage == .welcome { + Text("Retour") + } else { + Text("Annuler") + } + } + } + } + + private var validateButton: some View { + Group { + if self.navigationVM.showProfileEditor { + Button( + action: { + // Save changes and leave + self.company.saveProfileChanges(.user) + self.dismiss() + }, + label: { + self.validateButtonLabel + } + ) + } else if self.viewRouter.currentPage == .welcome { + EmptyView() + } else { + // User Selector before launching an activity + Button( + action: { + self.dismiss() + hideKeyboard() + self.company.saveProfileChanges(.user) + self.company.assignCurrentProfiles() + }, + label: { + self.validateButtonLabel + } + ) + } + } + .disabled(self.company.bufferUser.name.isEmpty) + } + + private var validateButtonLabel: some View { + HStack(spacing: 4) { + Image(systemName: "checkmark.circle") + Text("Enregistrer") + } + + .contentShape(Rectangle()) + } +} + +// MARK: - CreateUserProfileView_Previews + +struct CreateUserProfileView_Previews: PreviewProvider { + static var previews: some View { + CreateUserProfileViewDeprecated() + .environmentObject(CompanyViewModelDeprecated()) + .environmentObject(ViewRouterDeprecated()) + .environmentObject(NavigationViewModelDeprecated()) + .environmentObject(UIMetrics()) + .previewInterfaceOrientation(.landscapeLeft) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/JobPicker/Components/JobPickerStore+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/JobPicker/Components/JobPickerStore+DEPRECATED.swift new file mode 100644 index 0000000000..3667aa7036 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/JobPicker/Components/JobPickerStore+DEPRECATED.swift @@ -0,0 +1,42 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct JobPickerStoreDeprecated: View { + // MARK: Internal + + @Binding var selectedJobs: [String] + + var body: some View { + ScrollView(showsIndicators: false) { + VStack(spacing: 40) { + let columns = Array(repeating: GridItem(), count: 3) + LazyVGrid(columns: columns, spacing: 40) { + ForEach(ProfessionsDeprecated.allCases) { profession in + Toggle(isOn: .constant(self.selectedJobs.contains(profession.name))) { + Text(profession.name) + .font(.subheadline) + } + .toggleStyle( + JobPickerToggleStyleDeprecated(action: { + self.jobSelection(profession: profession.name) + })) + } + } + .padding(.vertical, 30) + } + } + } + + // MARK: Private + + private func jobSelection(profession: String) { + if self.selectedJobs.contains(profession) { + self.selectedJobs.removeAll(where: { profession == $0 }) + } else { + self.selectedJobs.append(profession) + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/JobPicker/Components/JobTag+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/JobPicker/Components/JobTag+DEPRECATED.swift new file mode 100644 index 0000000000..1374331ca2 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/JobPicker/Components/JobTag+DEPRECATED.swift @@ -0,0 +1,44 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - JobTagDeprecated + +struct JobTagDeprecated: View { + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + @State var profession: String + + var body: some View { + HStack(spacing: 12) { + Text(self.profession) + .padding(.leading, 4) + Button { + self.company.bufferTeacher.jobs.removeAll(where: { self.profession == $0 }) + } label: { + Image(systemName: "multiply.square.fill") + } + } + .font(.footnote) + .foregroundColor(.white) + .padding(5) + .background( + DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor, + in: RoundedRectangle(cornerRadius: 6, style: .circular) + ) + } +} + +// MARK: - JobTag_Previews + +struct JobTag_Previews: PreviewProvider { + static var previews: some View { + JobTagDeprecated(profession: "Accompagnant des élèves en situation de handicap") + .environmentObject(CompanyViewModelDeprecated()) + .environmentObject(UIMetrics()) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/JobPicker/JobPicker+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/JobPicker/JobPicker+DEPRECATED.swift new file mode 100644 index 0000000000..a5a4ac23ff --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/JobPicker/JobPicker+DEPRECATED.swift @@ -0,0 +1,114 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - JobPickerDeprecated + +struct JobPickerDeprecated: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + @EnvironmentObject var viewRouter: ViewRouterDeprecated + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + @Environment(\.dismiss) var dismiss + + @FocusState var focusedField: FormField? + + var body: some View { + ZStack { + Color.white.edgesIgnoringSafeArea(.top) + + JobPickerStoreDeprecated(selectedJobs: self.$selectedJobs) + .onAppear { + self.selectedJobs = self.company.bufferTeacher.jobs + } + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .safeAreaInset(edge: .bottom) { + self.customJobTextField + } + .toolbar { + ToolbarItem(placement: .principal) { self.navigationTitle } + ToolbarItem(placement: .navigationBarLeading) { self.adaptiveBackButton } + ToolbarItem(placement: .navigationBarTrailing) { self.validateButton } + } + } + .toolbarBackground(self.navigationVM.showProfileEditor ? .visible : .automatic, for: .navigationBar) + } + + // MARK: Private + + @State private var otherJobText: String = "" + @State private var isEditing = false + @State private var selectedJobs: [String] = [] + + private var customJobTextField: some View { + VStack(spacing: 0) { + Divider() + .padding(.horizontal, 20) + LekaTextFieldDeprecated(label: "Autre (préciser)", entry: self.$otherJobText, isEditing: self.$isEditing, type: .name) { + if !self.otherJobText.isEmpty || !self.company.bufferTeacher.jobs.contains(self.otherJobText) { + self.selectedJobs.append(self.otherJobText) + } + } + .padding(.vertical, 30) + } + .edgesIgnoringSafeArea(.bottom) + .background( + Rectangle() + .fill(.white) + .edgesIgnoringSafeArea(.all) + ) + } + + private var navigationTitle: some View { + Text("Sélectionnez vos professions") + .font(.headline) + } + + private var adaptiveBackButton: some View { + Button { + // go back without saving + self.dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "chevron.left") + if self.viewRouter.currentPage == .welcome { + Text("Retour") + } else { + Text("Annuler") + } + } + } + } + + private var validateButton: some View { + Button { + self.company.bufferTeacher.jobs = self.selectedJobs + self.dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "checkmark.circle") + Text("Valider la sélection") + } + } + .disabled(self.selectedJobs.isEmpty) + .disabled(self.isEditing) + } +} + +// MARK: - JobPicker_Previews + +struct JobPicker_Previews: PreviewProvider { + static var previews: some View { + JobPickerDeprecated() + .environmentObject(CompanyViewModelDeprecated()) + .environmentObject(ViewRouterDeprecated()) + .environmentObject(UIMetrics()) + .previewInterfaceOrientation(.landscapeLeft) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/ProfileSets/IdentificationIsNeededAlertLabel.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/ProfileSets/IdentificationIsNeededAlertLabel.swift new file mode 100644 index 0000000000..433121d5db --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/ProfileSets/IdentificationIsNeededAlertLabel.swift @@ -0,0 +1,24 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct IdentificationIsNeededAlertLabel: View { + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var viewRouter: ViewRouterDeprecated + + var body: some View { + Button { + self.settings.showConnectInvite.toggle() + } label: { + Text("Non") + } + + Button { + self.viewRouter.currentPage = .welcome + } label: { + Text("Oui") + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/ProfileSets/ProfileSet_Teachers+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/ProfileSets/ProfileSet_Teachers+DEPRECATED.swift new file mode 100644 index 0000000000..e2f6d238b8 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/ProfileSets/ProfileSet_Teachers+DEPRECATED.swift @@ -0,0 +1,150 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct ProfileSet_TeachersDeprecated: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var viewRouter: ViewRouterDeprecated + @EnvironmentObject var metrics: UIMetrics + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + + var body: some View { + VStack(spacing: 0) { + self.header + + // Separator + Rectangle() + .fill(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) + .frame(height: 1) + .frame(maxWidth: self.navigationVM.showProfileEditor ? .infinity : 460) + + // Avatars + self.availableProfiles + } + .task { + self.company.sortProfiles(.teacher) + } + .navigationBarTitleDisplayMode(.inline) + .preferredColorScheme(.light) + .frame(minWidth: 460) + .navigationDestination(isPresented: self.$navigateToTeacherCreation) { + CreateTeacherProfileViewDeprecated() + } + .sheet(isPresented: self.$showEditProfileTeacher) { + NavigationStack { + CreateTeacherProfileViewDeprecated() + } + } + .alert("Mode découverte", isPresented: self.$settings.showConnectInvite) { + IdentificationIsNeededAlertLabel() + } message: { + Text( + "Ce mode ne vous permet pas de créer des profils ou d'enregistrer votre utilisation de l'application. \nVoulez-vous vous identifier ?" + ) + } + } + + // MARK: Private + + @State private var showEditProfileTeacher: Bool = false + @State private var navigateToTeacherCreation: Bool = false + + private var editButton: some View { + Button { + if self.settings.companyIsConnected { + self.company.editProfile(.teacher) + self.showEditProfileTeacher.toggle() + } else { + self.settings.showConnectInvite.toggle() + } + } label: { + Image(systemName: "pencil") + } + .buttonStyle(CircledIcon_NoFeedback_ButtonStyleDeprecated(font: .body)) + } + + private var header: some View { + HStack(spacing: 20) { + if !self.navigationVM.showProfileEditor { + Spacer() + } + Text("Qui êtes-vous ?") + .font(.headline) + if self.navigationVM.showProfileEditor { + Spacer() + } + self.addButton() + if !self.navigationVM.showProfileEditor { + Spacer() + } + if self.navigationVM.showProfileEditor { + self.editButton + } + } + .padding(20) + } + + private var teachersSet: some View { + ForEach(self.company.currentCompany.teachers) { teacher in + TeacherSet_AvatarCellDeprecated(teacher: teacher) + } + } + + @ViewBuilder + private var availableProfiles: some View { + if !self.navigationVM.showProfileEditor, self.sixMax() { + VStack { + Spacer() + HStack(spacing: 40) { + self.teachersSet + } + .offset(y: -100) + Spacer() + } + } else { + ScrollView(showsIndicators: false) { + let columns = Array( + repeating: GridItem(spacing: 20), count: navigationVM.showProfileEditor ? 3 : 6 + ) + LazyVGrid(columns: columns, spacing: 20) { + self.teachersSet + } + .padding(.bottom, 20) + } + ._safeAreaInsets(EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)) + } + } + + // check if less than 7 profiles to display in order to adapt Layout (HStack vs. Scrollable Grid) + private func sixMax() -> Bool { + self.company.currentCompany.teachers.count < 7 + } + + @ViewBuilder + private func addButton() -> some View { + Button { + if self.viewRouter.currentPage == .welcome { + // Existing company is connected, we're in the selector here + self.company.resetBufferProfile(.teacher) + self.navigateToTeacherCreation.toggle() + } else { + if self.settings.companyIsConnected { + self.company.editingProfile = false + self.company.resetBufferProfile(.teacher) + self.showEditProfileTeacher.toggle() + } else { + self.settings.showConnectInvite.toggle() + } + } + } label: { + Image(systemName: "plus") + } + .buttonStyle(CircledIcon_NoFeedback_ButtonStyleDeprecated(font: .body)) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/ProfileSets/ProfileSet_Users+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/ProfileSets/ProfileSet_Users+DEPRECATED.swift new file mode 100644 index 0000000000..a2fb9ba137 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/ProfileSets/ProfileSet_Users+DEPRECATED.swift @@ -0,0 +1,146 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct ProfileSet_UsersDeprecated: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + @Environment(\.dismiss) var dismiss + + var body: some View { + VStack(spacing: 0) { + self.header + + // Separator + Rectangle() + .fill(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) + .frame(height: 1) + .frame(maxWidth: self.navigationVM.showProfileEditor ? .infinity : 460) + + // Avatars + self.availableProfiles + } + .task { + self.company.sortProfiles(.user) + } + .navigationBarTitleDisplayMode(.inline) + .preferredColorScheme(.light) + .frame(minWidth: 460) + .sheet(isPresented: self.$showEditProfileUser) { + NavigationStack { + CreateUserProfileViewDeprecated() + } + } + .alert("Mode découverte", isPresented: self.$settings.showConnectInvite) { + IdentificationIsNeededAlertLabel() + } message: { + Text( + "Ce mode ne vous permet pas de créer des profils ou d'enregistrer votre utilisation de l'application. \nVoulez-vous vous identifier ?" + ) + } + } + + // MARK: Private + + @State private var showEditProfileUser: Bool = false + + private var editButton: some View { + Button { + if self.settings.companyIsConnected { + self.company.editProfile(.user) + self.showEditProfileUser.toggle() + } else { + self.settings.showConnectInvite.toggle() + } + } label: { + Image(systemName: "pencil") + } + .buttonStyle(CircledIcon_NoFeedback_ButtonStyleDeprecated(font: .body)) + .disabled( + self.company.getProfileDataFor( + .user, + id: self.company.profilesInUse[.user]! + )[0] == DesignKitAsset.Avatars.questionMarkBlue.name + && !self.company.profileIsSelected(.user) + ) + } + + private var addButton: some View { + Button { + if self.settings.companyIsConnected { + self.company.editingProfile = false + self.company.resetBufferProfile(.user) + self.showEditProfileUser.toggle() + } else { + self.settings.showConnectInvite.toggle() + } + } label: { + Image(systemName: "plus") + } + .buttonStyle(CircledIcon_NoFeedback_ButtonStyleDeprecated(font: .body)) + } + + private var header: some View { + HStack(spacing: 20) { + if !self.navigationVM.showProfileEditor { + Spacer() + } + Text("Qui accompagnez-vous?") + .font(.body) + if self.navigationVM.showProfileEditor { + Spacer() + } + self.addButton + if !self.navigationVM.showProfileEditor { + Spacer() + } + if self.navigationVM.showProfileEditor { + self.editButton + } + } + .padding(20) + } + + private var usersSet: some View { + ForEach(self.company.currentCompany.users) { user in + UserSet_AvatarCellDeprecated(user: user) + } + } + + @ViewBuilder + private var availableProfiles: some View { + if !self.navigationVM.showProfileEditor, self.sixMax() { + VStack { + Spacer() + HStack(spacing: 40) { + self.usersSet + } + .offset(y: -100) + Spacer() + } + } else { + ScrollView(showsIndicators: false) { + let columns = Array( + repeating: GridItem(spacing: 20), count: navigationVM.showProfileEditor ? 3 : 6 + ) + LazyVGrid(columns: columns, spacing: 20) { + self.usersSet + } + .padding(.bottom, 20) + } + ._safeAreaInsets(EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)) + } + } + + // check if less than 7 profiles to display in order to adapt Layout (HStack vs. Scrollable Grid) + private func sixMax() -> Bool { + self.company.currentCompany.users.count < 7 + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/ProfileSets/TeacherSetComponents/TeacherSet_AvatarCell+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/ProfileSets/TeacherSetComponents/TeacherSet_AvatarCell+DEPRECATED.swift new file mode 100644 index 0000000000..65a290a3e2 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/ProfileSets/TeacherSetComponents/TeacherSet_AvatarCell+DEPRECATED.swift @@ -0,0 +1,109 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct TeacherSet_AvatarCellDeprecated: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var viewRouter: ViewRouterDeprecated + @EnvironmentObject var metrics: UIMetrics + + let teacher: TeacherDeprecated + + var body: some View { + Button { + withAnimation { + self.company.selectedProfiles[.teacher] = self.teacher.id + } + if self.settings.companyIsLoggingIn { + self.company.assignCurrentProfiles() + self.settings.companyIsLoggingIn = false + self.viewRouter.currentPage = .home + } + } label: { + VStack(spacing: 0) { + ZStack(alignment: .topTrailing) { + // Selection Indicator + self.selectionIndicator(id: self.teacher.id) + // Avatar + Circle() + .fillDeprecated( + DesignKitAsset.Colors.lekaLightGray.swiftUIColor, + strokeBorder: .white, + lineWidth: 3 + ) + .overlay(content: { + Image(self.teacher.avatar, bundle: Bundle(for: DesignKitResources.self)) + .resizable() + .aspectRatio(contentMode: .fill) + .clipShape(Circle()) + .padding(2) + }) + } + .frame(height: 108) + .padding(10) + + Text(self.teacher.name) + .font(.body) + .allowsTightening(true) + .lineLimit(2) + .padding(.horizontal, 14) + .foregroundColor( + self.company.profileIsCurrent(.teacher, id: self.teacher.id) + ? Color.white : DesignKitAsset.Colors.lekaDarkGray.swiftUIColor + ) + .padding(2) + .frame(minWidth: 108) + .background(content: { + RoundedRectangle(cornerRadius: self.metrics.btnRadius) + .stroke(.white, lineWidth: 2) + }) + .background( + self.company.profileIsCurrent(.teacher, id: self.teacher.id) + ? DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor + : DesignKitAsset.Colors.lekaLightGray.swiftUIColor, + in: RoundedRectangle(cornerRadius: self.metrics.btnRadius) + ) + } + } + .buttonStyle(NoFeedback_ButtonStyleDeprecated()) + } + + // MARK: Private + + private func selectionIndicator(id: UUID) -> some View { + // TODO(@ladislas): review logic in the future + let lineWidth: CGFloat = { + guard self.company.selectedProfiles[.teacher] == id else { + guard self.company.profileIsCurrent(.teacher, id: id) else { + return 0 + } + return 10 + } + return 10 + }() + + let dash: [CGFloat] = { + guard self.company.profileIsCurrent(.teacher, id: id) else { + return [10, 4] + } + return [10, 0] + }() + + return Circle() + .stroke( + DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor, + style: StrokeStyle( + lineWidth: lineWidth, + lineCap: .butt, + lineJoin: .round, + dash: dash + ) + ) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/ProfileSets/UserSetComponents/UserSet_AvatarCell+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/ProfileSets/UserSetComponents/UserSet_AvatarCell+DEPRECATED.swift new file mode 100644 index 0000000000..248ea588bd --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Profiles/ProfilesComponents/ProfileSets/UserSetComponents/UserSet_AvatarCell+DEPRECATED.swift @@ -0,0 +1,138 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct UserSet_AvatarCellDeprecated: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + + let user: UserDeprecated + + var body: some View { + Button { + withAnimation { + self.company.selectedProfiles[.user] = self.user.id + } + // Next context is within the userSelector right before launching a game + if !self.navigationVM.showProfileEditor { + self.company.assignCurrentProfiles() + self.navigationVM.pathToGame.append(PathsToGame.game) + } + } label: { + VStack(spacing: 0) { + ZStack(alignment: .topTrailing) { + // Selection Indicator + self.selectionIndicator(id: self.user.id) + // Avatar + Circle() + .fillDeprecated( + DesignKitAsset.Colors.lekaLightGray.swiftUIColor, + strokeBorder: .white, + lineWidth: 3 + ) + .overlay(content: { + Image(self.user.avatar, bundle: Bundle(for: DesignKitResources.self)) + .resizable() + .aspectRatio(contentMode: .fill) + .clipShape(Circle()) + .padding(2) + }) + // Reinforcer Badge + ZStack(alignment: .topTrailing) { + Circle() + .fill(DesignKitAsset.Colors.lekaLightGray.swiftUIColor) + Image(uiImage: self.company.getReinforcerFor(index: self.user.reinforcer)) + .resizable() + .renderingMode(.original) + .aspectRatio(contentMode: .fit) + .padding(2) + .background(DesignKitAsset.Colors.lekaLightGray.swiftUIColor, in: Circle()) + + Circle() + .stroke(.white, lineWidth: 3) + } + .frame(maxWidth: 40, maxHeight: 40) + .offset(x: 6, y: -6) + } + .frame(height: 108) + .padding(10) + + Text(self.user.name) + .font(.body) + .allowsTightening(true) + .lineLimit(2) + .padding(.horizontal, 14) + .foregroundColor( + self.company.profileIsCurrent(.user, id: self.user.id) + ? Color.white : DesignKitAsset.Colors.lekaDarkGray.swiftUIColor + ) + .padding(2) + .frame(minWidth: 108) + .background(content: { + RoundedRectangle(cornerRadius: self.metrics.btnRadius) + .stroke(.white, lineWidth: 2) + }) + .background( + self.company.profileIsCurrent(.user, id: self.user.id) + ? DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor + : DesignKitAsset.Colors.lekaLightGray.swiftUIColor, + in: RoundedRectangle(cornerRadius: self.metrics.btnRadius) + ) + } + } + .buttonStyle(NoFeedback_ButtonStyleDeprecated()) + } + + // MARK: Private + + @ViewBuilder + private func selectionIndicator(id: UUID) -> some View { + // TODO(@ladislas): review logic in the future + let lineWidth: CGFloat = { + guard self.company.selectedProfiles[.user] == id else { + guard self.company.profileIsCurrent(.user, id: id) else { + return 0 + } + return 10 + } + return 10 + }() + + let dash: [CGFloat] = { + guard self.company.profileIsCurrent(.user, id: id) else { + return [10, 4] + } + return [10, 0] + }() + + Circle() + .stroke( + DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor, + style: StrokeStyle( + lineWidth: lineWidth, + lineCap: .butt, + lineJoin: .round, + dash: dash + ) + ) + Circle() + .stroke( + DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor, + style: StrokeStyle( + lineWidth: lineWidth, + lineCap: .butt, + lineJoin: .round, + dash: dash + ) + ) + .frame(maxWidth: 40, maxHeight: 40) + .offset(x: 6, y: -6) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Settings/Components/SettingsSection_Account+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Settings/Components/SettingsSection_Account+DEPRECATED.swift new file mode 100644 index 0000000000..85292a009e --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Settings/Components/SettingsSection_Account+DEPRECATED.swift @@ -0,0 +1,55 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct SettingsSection_AccountDeprecated: View { + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var robotVM: RobotViewModel + @EnvironmentObject var viewRouter: ViewRouterDeprecated + + var body: some View { + Section { + Group { + Button("Se déconnecter") { + self.settings.showConfirmDisconnection.toggle() + } + .foregroundColor(.blue) + Button("Supprimer le compte") { + self.settings.showConfirmDeleteAccount.toggle() + } + .foregroundColor(.red) + } + .frame(maxHeight: 52) + } + .alert("Déconnexion", isPresented: self.$settings.showConfirmDisconnection) { + Button(role: .destructive) { + self.viewRouter.currentPage = .welcome + self.company.disconnect() + self.robotVM.disconnect() + self.settings.companyIsConnected = false + } label: { + Text("Se déconnecter") + } + } message: { + Text("Vous êtes sur le point de vous déconnecter.") + } + .alert("Supprimer le compte", isPresented: self.$settings.showConfirmDeleteAccount) { + Button(role: .destructive) { + // For now... + self.viewRouter.currentPage = .welcome + self.company.disconnect() + self.robotVM.disconnect() + self.settings.companyIsConnected = false + } label: { + Text("Supprimer") + } + } message: { + Text( // swiftlint:disable:next line_length + "Vous êtes sur le point de supprimer votre compte et toutes les données qu'il contient. \nCette action est irreversible. \nVoulez-vous continuer ?" + ) + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Settings/Components/SettingsSection_Credentials+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Settings/Components/SettingsSection_Credentials+DEPRECATED.swift new file mode 100644 index 0000000000..dfb5e3032b --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Settings/Components/SettingsSection_Credentials+DEPRECATED.swift @@ -0,0 +1,35 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct SettingsSection_CredentialsDeprecated: View { + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + Section { + Group { + LabeledContent { + Text(self.company.currentCompany.mail) + .font(.footnote) + .multilineTextAlignment(.leading) + } label: { + Text("Adresse mail du compte") + } + Link(destination: URL(string: "https://leka.io")!) { + Text("Modifier l'email et le mot de passe") + .foregroundColor(.blue) + } + } + .frame(maxHeight: 52) + } header: { + Text("Compte") + .font(.subheadline) + .headerProminence(.increased) + .padding(.top, 20) + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Settings/Components/SettingsSection_Exploratory+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Settings/Components/SettingsSection_Exploratory+DEPRECATED.swift new file mode 100644 index 0000000000..455d7bb1b4 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Settings/Components/SettingsSection_Exploratory+DEPRECATED.swift @@ -0,0 +1,34 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct SettingsSection_ExploratoryDeprecated: View { + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + Section { + LabeledContent { + Toggle("Mode exploratoire", isOn: self.$settings.exploratoryModeIsOn) + .toggleStyle(SwitchToggleStyle(tint: DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor)) + .labelsHidden() + } label: { + Text("Le mode exploratoire vous permet de découvrir les contenus sans enregistrer l'utilisation") + .font(.footnote) + .foregroundColor(DesignKitAsset.Colors.lekaDarkGray.swiftUIColor) + .frame(maxWidth: 300) + } + .frame(maxHeight: 52) + } header: { + HStack { + Image(systemName: "binoculars.fill") + Text("Mode exploratoire") + } + .font(.subheadline) + .headerProminence(.increased) + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Settings/Components/SettingsSection_Profiles+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Settings/Components/SettingsSection_Profiles+DEPRECATED.swift new file mode 100644 index 0000000000..adbbea9d4e --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Settings/Components/SettingsSection_Profiles+DEPRECATED.swift @@ -0,0 +1,73 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct SettingsSection_ProfilesDeprecated: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + Section { + Group { + self.avatarsRow(.teacher) + self.avatarsRow(.user) + } + .frame(maxHeight: 52) + } header: { + Text("Profils") + .font(.body) + .headerProminence(.increased) + } + } + + // MARK: Private + + private func avatar(_ name: String) -> some View { + Image(name, bundle: Bundle(for: DesignKitResources.self)) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxWidth: 30) + .background(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) + .clipShape(Circle()) + .overlay(Circle().stroke(.white, lineWidth: 2)) + } + + private func remainingProfiles(_ remainder: Int) -> some View { + Circle() + .fill(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) + .frame(maxWidth: 30) + .overlay( + Text("+\(remainder)") + .foregroundColor(.white) + .font(.footnote) + .clipShape(Circle()) + ) + .overlay(Circle().stroke(.white, lineWidth: 2)) + } + + private func avatarsRow(_ type: UserTypeDeprecated) -> some View { + LabeledContent { + HStack(spacing: -10) { + ForEach(self.company.getAllAvatarsOf(type).prefix(10), id: \.self) { item in + self.avatar(item.first!.value) + } + if self.company.getAllAvatarsOf(type).count > 10 { + let remainder: Int = self.company.getAllAvatarsOf(type).count - 10 + self.remainingProfiles(remainder) + } + Spacer() + } + .frame(minWidth: 320, maxWidth: 320) + } label: { + Text( + "Profils \(type == .teacher ? "accompagnants" : "utilisateurs") (\(self.company.getAllAvatarsOf(type).count))" + ) + .foregroundColor(DesignKitAsset.Colors.lekaDarkGray.swiftUIColor) + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Settings/SettingsView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Settings/SettingsView+DEPRECATED.swift new file mode 100644 index 0000000000..aa803f85fb --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Settings/SettingsView+DEPRECATED.swift @@ -0,0 +1,80 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - SettingsViewDeprecated + +struct SettingsViewDeprecated: View { + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + @Environment(\.dismiss) var dismiss + + var body: some View { + NavigationStack { + ZStack { + DesignKitAsset.Colors.lekaLightGray.swiftUIColor.ignoresSafeArea() + + Form { + Group { + SettingsSection_CredentialsDeprecated() + SettingsSection_ExploratoryDeprecated() + SettingsSection_ProfilesDeprecated() + SettingsSection_AccountDeprecated() + } + .padding(.horizontal, 20) + } + .scrollDisabled(true) + .padding(.horizontal, 10) + .formStyle(.grouped) + .font(.headline) + } + .interactiveDismissDisabled() + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .toolbarBackground(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor, for: .navigationBar) + .toolbar { + ToolbarItem(placement: .principal) { + HStack(spacing: 4) { + Text("Réglages") + if self.settings.companyIsConnected, self.settings.exploratoryModeIsOn { + Image(systemName: "binoculars.fill") + } + } + .font(.headline) + .foregroundColor(.white) + } + ToolbarItem(placement: .navigationBarLeading) { + Button( + action: { + self.dismiss() + }, + label: { + Text("Fermer") + .foregroundColor(.white) + } + ) + } + } + } + } +} + +// MARK: - SettingsView_Previews + +struct SettingsView_Previews: PreviewProvider { + @State static var open: Bool = true + + static var previews: some View { + DesignKitAsset.Colors.lekaLightBlue.swiftUIColor + .ignoresSafeArea() + .sheet(isPresented: $open) { + SettingsViewDeprecated() + .environmentObject(SettingsViewModelDeprecated()) + .environmentObject(UIMetrics()) + } + .previewInterfaceOrientation(.landscapeLeft) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/Components/Shared/SidebarAvatarNameLabel.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/Components/Shared/SidebarAvatarNameLabel.swift new file mode 100644 index 0000000000..681b3d0e23 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/Components/Shared/SidebarAvatarNameLabel.swift @@ -0,0 +1,33 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct SidebarAvatarNameLabel: View { + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + let type: UserTypeDeprecated + + var body: some View { + Text( + self.company.getProfileDataFor( + self.type, + id: self.company.profilesInUse[self.type]! + )[1] + ) + .font(.subheadline) + .allowsTightening(true) + .lineLimit(2) + .padding(.vertical, 2) + .padding(.horizontal, 6) + .frame(minWidth: 100) + .background(.white, in: RoundedRectangle(cornerRadius: self.metrics.btnRadius)) + } +} + +#Preview { + SidebarAvatarNameLabel(type: .user) +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/Components/Shared/SidebarAvatarView.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/Components/Shared/SidebarAvatarView.swift new file mode 100644 index 0000000000..250242dd51 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/Components/Shared/SidebarAvatarView.swift @@ -0,0 +1,43 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct SidebarAvatarView: View { + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + + let type: UserTypeDeprecated + + var body: some View { + Circle() + .fill(DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) + .overlay( + Image( + self.company.getProfileDataFor( + self.type, + id: self.company.profilesInUse[self.type]! + )[0], + bundle: Bundle(for: DesignKitResources.self) + ) + .resizable() + .aspectRatio(contentMode: .fill) + .clipShape(Circle()) + ) + .overlay( + Circle() + .fill(.black) + .opacity(self.settings.exploratoryModeIsOn ? 0.3 : 0) + ) + .overlay { + Circle() + .stroke(.white, lineWidth: 4) + } + } +} + +#Preview { + SidebarAvatarView(type: .teacher) +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/Components/TeacherSidebarAvatarCell.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/Components/TeacherSidebarAvatarCell.swift new file mode 100644 index 0000000000..1f23a77333 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/Components/TeacherSidebarAvatarCell.swift @@ -0,0 +1,28 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct TeacherSidebarAvatarCell: View { + @EnvironmentObject var settings: SettingsViewModelDeprecated + + var body: some View { + HStack { + Spacer() + VStack(spacing: 0) { + ZStack(alignment: .topTrailing) { + SidebarAvatarView(type: .teacher) + } + .frame(height: self.settings.exploratoryModeIsOn ? 58 : 72) + .offset(x: self.settings.exploratoryModeIsOn ? 26 : 0) + .padding(10) + + if !self.settings.exploratoryModeIsOn { + SidebarAvatarNameLabel(type: .teacher) + } + } + Spacer() + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/Components/TickPic.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/Components/TickPic.swift new file mode 100644 index 0000000000..54869e4f9c --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/Components/TickPic.swift @@ -0,0 +1,43 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct TickPic: View { + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + + var body: some View { + HStack(alignment: .top) { + self.imageFromContext() + .resizable() + .renderingMode(self.settings.exploratoryModeIsOn ? .template : .original) + .aspectRatio(contentMode: .fit) + .foregroundColor(.white) + .padding(self.settings.exploratoryModeIsOn ? 20 : 0) + .fontWeight(.light) + .background( + self.settings.exploratoryModeIsOn ? DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor : .clear, in: Circle() + ) + .overlay( + Circle() + .stroke(.white, lineWidth: 3) + .opacity(self.settings.exploratoryModeIsOn ? 1 : 0) + ) + .frame(maxWidth: self.settings.exploratoryModeIsOn ? 72 : 44) + .offset(y: self.settings.exploratoryModeIsOn ? 4 : -4) + } + } + + func imageFromContext() -> Image { + guard self.settings.exploratoryModeIsOn else { + guard self.company.profileIsAssigned(.user) || !self.settings.companyIsConnected else { + return DesignKitAsset.Images.cross.swiftUIImage + } + return DesignKitAsset.Images.tick.swiftUIImage + } + return Image(systemName: "binoculars.fill") + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/Components/UserSidebarAvatarCell.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/Components/UserSidebarAvatarCell.swift new file mode 100644 index 0000000000..04d170c018 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/Components/UserSidebarAvatarCell.swift @@ -0,0 +1,61 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct UserSidebarAvatarCell: View { + // MARK: Internal + + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + HStack { + Spacer() + VStack(spacing: 0) { + ZStack(alignment: .topTrailing) { + SidebarAvatarView(type: .user) + self.avatarAccessoryView + } + .frame(height: self.settings.exploratoryModeIsOn ? 58 : 72) + .offset(x: self.settings.exploratoryModeIsOn ? -26 : 0) + .padding(10) + + if !self.settings.exploratoryModeIsOn { + SidebarAvatarNameLabel(type: .user) + } + } + Spacer() + } + } + + // MARK: Private + + @ViewBuilder private var avatarAccessoryView: some View { + if !self.settings.companyIsConnected || (!self.company.profileIsAssigned(.user) && !self.settings.exploratoryModeIsOn) { + Image(systemName: "exclamationmark.circle.fill") + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .red) + .font(.title) + .frame(maxWidth: 22, maxHeight: 22) + .offset(x: 2, y: -2) + } else if self.company.profileIsAssigned(.user) { + Image(uiImage: self.company.getReinforcerFor(index: self.company.getCurrentUserReinforcer())) + .resizable() + .renderingMode(.original) + .aspectRatio(contentMode: .fit) + .frame(maxWidth: self.settings.exploratoryModeIsOn ? 24 : 33) + .background(.white) + .clipShape(Circle()) + .overlay( + Circle() + .fill(.black) + .opacity(self.settings.exploratoryModeIsOn ? 0.3 : 0) + ) + .overlay(Circle().stroke(.white, lineWidth: 3)) + .offset(x: 6, y: self.settings.exploratoryModeIsOn ? -8 : -12) + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/GoToProfileEditorButton+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/GoToProfileEditorButton+DEPRECATED.swift new file mode 100644 index 0000000000..5249943e29 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/ProfilesButton/GoToProfileEditorButton+DEPRECATED.swift @@ -0,0 +1,65 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - GoToProfileEditorButtonDeprecated + +struct GoToProfileEditorButtonDeprecated: View { + // MARK: Internal + + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + + var body: some View { + Button { + if self.settings.exploratoryModeIsOn { + self.settings.showSwitchOffExploratoryAlert.toggle() + } else { + self.navigationVM.showProfileEditor.toggle() + } + } label: { + VStack(spacing: 5) { + HStack(alignment: .top) { + Spacer() + TeacherSidebarAvatarCell() + UserSidebarAvatarCell() + Spacer() + } + .overlay(TickPic()) + + if self.settings.exploratoryModeIsOn { + self.exploratoryModeLabel + } + } + } + .frame(minHeight: 135) + .contentShape(Rectangle()) + .animation(.default, value: self.settings.exploratoryModeIsOn) + } + + // MARK: Private + + private var exploratoryModeLabel: some View { + Text("Mode exploratoire") + .font(.body) + .foregroundColor(DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor) + .padding(.vertical, 2) + .padding(.horizontal, 6) + .background(.white, in: RoundedRectangle(cornerRadius: self.metrics.btnRadius)) + } +} + +// MARK: - GoToProfileEditorButton_Previews + +struct GoToProfileEditorButton_Previews: PreviewProvider { + static var previews: some View { + GoToProfileEditorButtonDeprecated() + .environmentObject(SettingsViewModelDeprecated()) + .environmentObject(UIMetrics()) + .environmentObject(NavigationViewModelDeprecated()) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/RobotButton/Components/RobotConnectionIndicator+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/RobotButton/Components/RobotConnectionIndicator+DEPRECATED.swift new file mode 100644 index 0000000000..66b189b076 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/RobotButton/Components/RobotConnectionIndicator+DEPRECATED.swift @@ -0,0 +1,81 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import RobotKit +import SwiftUI + +struct RobotConnectionIndicatorDeprecated: View { + // MARK: Internal + + @EnvironmentObject var metrics: UIMetrics + + @StateObject var robotViewModel: ConnectedRobotInformationViewModel = .init() + + var body: some View { + ZStack { + Circle() + .fill( + self.robotViewModel.isConnected + ? DesignKitAsset.Colors.lekaGreen.swiftUIColor : DesignKitAsset.Colors.lekaDarkGray.swiftUIColor + ) + .opacity(0.4) + + Image( + uiImage: + self.robotViewModel.isConnected + ? DesignKitAsset.Images.robotConnected.image : DesignKitAsset.Images.robotDisconnected.image + ) + .resizable() + .renderingMode(.original) + .aspectRatio(contentMode: .fit) + .frame(width: 44, height: 44, alignment: .center) + + Circle() + .stroke( + self.robotViewModel.isConnected + ? DesignKitAsset.Colors.lekaGreen.swiftUIColor + : DesignKitAsset.Colors.lekaDarkGray.swiftUIColor, + lineWidth: 4 + ) + .frame(width: 44, height: 44) + } + .frame(width: 67, height: 67, alignment: .center) + .background( + Circle() + .fill(DesignKitAsset.Colors.lekaGreen.swiftUIColor) + .frame(width: self.diameter, height: self.diameter) + .opacity(self.isAnimated ? 0.0 : 0.8) + .animation( + Animation.easeInOut(duration: 1.5).delay(5).repeatForever(autoreverses: false), value: self.diameter + ) + .opacity(self.robotViewModel.isConnected ? 1 : 0.0) + ) + .overlay( + alignment: .topTrailing, + content: { + self.badgeView + } + ) + .onAppear { + self.isAnimated = true + self.diameter = self.isAnimated ? 100 : 0 + } + } + + // MARK: Private + + @State private var isAnimated: Bool = false + @State private var diameter: CGFloat = 0 + + @ViewBuilder private var badgeView: some View { + if !self.robotViewModel.isConnected { + Image(systemName: "exclamationmark.circle.fill") + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .red) + .font(.title2) + .frame(maxWidth: 22, maxHeight: 22) + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/RobotButton/GoToRobotConnectButton+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/RobotButton/GoToRobotConnectButton+DEPRECATED.swift new file mode 100644 index 0000000000..77ee5cf52c --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/RobotButton/GoToRobotConnectButton+DEPRECATED.swift @@ -0,0 +1,83 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import RobotKit +import SwiftUI + +struct GoToRobotConnectButtonDeprecated: View { + // MARK: Internal + + @EnvironmentObject var metrics: UIMetrics + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + + @StateObject var robotViewModel: ConnectedRobotInformationViewModel = .init() + + var body: some View { + Button { + self.navigationVM.showRobotPicker.toggle() + } label: { + HStack(spacing: 10) { + RobotConnectionIndicatorDeprecated() + self.buttonContent + Spacer() + } + .padding(.horizontal, 15) + .padding(.vertical, 10) + .background(.white, in: RoundedRectangle(cornerRadius: 10, style: .continuous)) + .padding(10) + .frame(height: 100) + } + .contentShape(Rectangle()) + } + + // MARK: Private + + @ViewBuilder + private var buttonContent: some View { + if self.robotViewModel.isConnected { + VStack(alignment: .leading, spacing: 4) { + Text("Connecté à") + .font(.caption2) + Text(self.robotViewModel.name) + .font(.subheadline) + self.robotCharginStatusAndBattery + } + } else { + Text("Connectez-vous à votre Leka") + .font(.subheadline) + .multilineTextAlignment(.leading) + } + } + + private var robotCharginStatusAndBattery: some View { + HStack(spacing: 5) { + Text(verbatim: "LekaOS v\(self.robotViewModel.osVersion)") + .font(.footnote) + .foregroundColor(.gray) + + if self.robotViewModel.isCharging { + Image(systemName: "bolt.circle.fill") + .foregroundColor(.blue) + } else { + Image(systemName: "bolt.slash.circle") + .foregroundColor(.gray.opacity(0.6)) + } + + let battery = BatteryViewModel(level: robotViewModel.battery) + Image(systemName: battery.name) + .foregroundColor(battery.color) + Text(verbatim: "\(battery.level)%") + .font(.footnote) + .foregroundColor(.gray) + .monospacedDigit() + } + } +} + +#Preview { + GoToRobotConnectButtonDeprecated() + .environmentObject(UIMetrics()) + .environmentObject(NavigationViewModelDeprecated()) +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/SidebarHeaderView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/SidebarHeaderView+DEPRECATED.swift new file mode 100644 index 0000000000..33c3be7976 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/Header/SidebarHeaderView+DEPRECATED.swift @@ -0,0 +1,31 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct SidebarHeaderViewDeprecated: View { + // MARK: Internal + + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + VStack(spacing: 10) { + self.logoLeka + GoToProfileEditorButtonDeprecated() + GoToRobotConnectButtonDeprecated() + } + .frame(minHeight: 350, idealHeight: 350, maxHeight: 371) + } + + // MARK: Private + + private var logoLeka: some View { + DesignKitAsset.Assets.lekaLogo.swiftUIImage + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 60) + .padding(.top, 20) + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/SidebarSections+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/SidebarSections+DEPRECATED.swift new file mode 100644 index 0000000000..8a2c552bce --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/Components/SidebarSections+DEPRECATED.swift @@ -0,0 +1,61 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct SidebarSectionsDeprecated: View { + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + @EnvironmentObject var robotVM: RobotViewModel + @EnvironmentObject var company: CompanyViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + self.section(content: self.navigationVM.educContentList) + .padding(.horizontal) + } + + func sectionItem(_ item: SectionLabel) -> some View { + Button { + self.navigationVM.currentView = item.destination + // emty navigation Stacks + self.navigationVM.pathsFromHome = .init() + // reset user's choice to work without robot + self.robotVM.userChoseToPlayWithoutRobot = false + } label: { + HStack { + Label(item.label, systemImage: item.icon) + .font(.body) + Spacer() + } + .padding(10) + .foregroundColor( + self.navigationVM.currentView.rawValue == item.destination.rawValue + ? .white : DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor + ) + .frame(height: 44) + .background( + self.navigationVM.currentView.rawValue == item.destination.rawValue + ? DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor : .clear, + in: RoundedRectangle(cornerRadius: self.metrics.btnRadius, style: .continuous) + ) + .contentShape(Rectangle()) + } + } + + func section(content: ListModel) -> some View { + Section { + ForEach(content.sections.indices, id: \.self) { item in + self.sectionItem(content.sections[item]) + } + } header: { + VStack(alignment: .leading, spacing: 6) { + Text(content.title) + .font(.title2) + .padding(.vertical, 10) + Divider() + } + } + } +} diff --git a/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/SidebarView+DEPRECATED.swift b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/SidebarView+DEPRECATED.swift new file mode 100644 index 0000000000..7965c747c5 --- /dev/null +++ b/Apps/LekaApp/Sources/_OLDCodeBase/Views/Sidebar/SidebarView+DEPRECATED.swift @@ -0,0 +1,69 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct SidebarViewDeprecated: View { + // MARK: Internal + + @EnvironmentObject var navigationVM: NavigationViewModelDeprecated + @EnvironmentObject var settings: SettingsViewModelDeprecated + @EnvironmentObject var metrics: UIMetrics + + var body: some View { + ScrollView { + VStack { + SidebarHeaderViewDeprecated() + SidebarSectionsDeprecated() + Spacer() + VStack(spacing: 20) { + Spacer() + self.settingsButton + self.appVersionIndicator + } + .padding(.bottom, 20) + } + } + .ignoresSafeArea(.container, edges: .top) + .background(DesignKitAsset.Colors.lekaLightGray.swiftUIColor) + .task { + self.appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + self.buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String + } + } + + // MARK: Private + + @State private var appVersion: String? = "" + @State private var buildNumber: String? = "" + + @ViewBuilder + private var settingsButton: some View { + if self.settings.companyIsConnected { + Button { + self.navigationVM.showSettings.toggle() + } label: { + HStack { + Image(systemName: "gear") + Text("Réglages") + .font(.body) + } + .foregroundColor(DesignKitAsset.Colors.lekaDarkGray.swiftUIColor) + .frame(width: 200, height: 44) + .background(.white, in: RoundedRectangle(cornerRadius: self.metrics.btnRadius, style: .continuous)) + .contentShape(Rectangle()) + } + } else { + EmptyView() + } + } + + private var appVersionIndicator: some View { + Text("My Leka App - Version \(self.appVersion!) (\(self.buildNumber!))") + .foregroundColor(DesignKitAsset.Colors.lekaDarkGray.swiftUIColor) + .font(.caption2) + .frame(alignment: .bottom) + } +} diff --git a/Apps/LekaApp/Tests/LekaAppTests.swift b/Apps/LekaApp/Tests/LekaAppTests.swift deleted file mode 100644 index 3bb277d364..0000000000 --- a/Apps/LekaApp/Tests/LekaAppTests.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation -import XCTest - -final class LekaAppTests: XCTestCase { - func test_twoPlusTwo_isFour() { - XCTAssertEqual(2+2, 4) - } -} diff --git a/Apps/LekaApp/Tests/LekaApp_Tests.swift b/Apps/LekaApp/Tests/LekaApp_Tests.swift new file mode 100644 index 0000000000..9e4ee19aa9 --- /dev/null +++ b/Apps/LekaApp/Tests/LekaApp_Tests.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +final class LekaApp_Tests: XCTestCase { + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/Apps/LekaEmotions/Project.swift b/Apps/LekaEmotions/Project.swift deleted file mode 100644 index b54a2d4246..0000000000 --- a/Apps/LekaEmotions/Project.swift +++ /dev/null @@ -1,13 +0,0 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -import ProjectDescription -import ProjectDescriptionHelpers - -// Creates our project using a helper function defined in ProjectDescriptionHelpers -let project = Project.app(name: "LekaEmotions", - platform: .iOS, - dependencies: [ - .project(target: "CoreUI", path: Path("../../Modules/CoreUI")), - ]) diff --git a/Apps/LekaEmotions/Sources/LekaEmotionsApp.swift b/Apps/LekaEmotions/Sources/LekaEmotionsApp.swift deleted file mode 100644 index 088fef0bee..0000000000 --- a/Apps/LekaEmotions/Sources/LekaEmotionsApp.swift +++ /dev/null @@ -1,11 +0,0 @@ -import SwiftUI -import CoreUI - -@main -struct LekaEmotionsApp: App { - var body: some Scene { - WindowGroup { - Hello("Leka Emotions", in: .pink) - } - } -} diff --git a/Apps/LekaEmotions/Tests/LekaEmotionsTests.swift b/Apps/LekaEmotions/Tests/LekaEmotionsTests.swift deleted file mode 100644 index 619880330d..0000000000 --- a/Apps/LekaEmotions/Tests/LekaEmotionsTests.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation -import XCTest - -final class LekaEmotionsTests: XCTestCase { - func test_twoPlusTwo_isFour() { - XCTAssertEqual(2+2, 4) - } -} diff --git a/Apps/LekaUpdater/Project.swift b/Apps/LekaUpdater/Project.swift index 11b33223a0..f91fc86355 100644 --- a/Apps/LekaUpdater/Project.swift +++ b/Apps/LekaUpdater/Project.swift @@ -1,13 +1,37 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap +// Leka - iOS Monorepo +// Copyright APF France handicap // SPDX-License-Identifier: Apache-2.0 +// swiftformat:disable acronyms + import ProjectDescription import ProjectDescriptionHelpers -// Creates our project using a helper function defined in ProjectDescriptionHelpers -let project = Project.app(name: "LekaUpdater", - platform: .iOS, - dependencies: [ - .project(target: "CoreUI", path: Path("../../Modules/CoreUI")), - ]) +let project = Project.app( + name: "LekaUpdater", + version: "1.4.1", + infoPlist: [ + "LEKA_OS_VERSION": "1.4.101", + "UISupportedInterfaceOrientations": ["UIInterfaceOrientationPortrait"], + "UISupportedInterfaceOrientations~ipad": [ + "UIInterfaceOrientationPortrait", + "UIInterfaceOrientationPortraitUpsideDown", + ], + "CFBundleURLTypes": [ + [ + "CFBundleTypeRole": "Editor", + "CFBundleURLName": "io.leka.apf.LekaUpdater", + "CFBundleURLSchemes": ["LekaUpdater"], + ], + ], + "LSApplicationQueriesSchemes": [ + "LekaApp", "com.googleusercontent.apps.224911845933-mv4tp4rstgjtvdqvbv5dl7defii1a7ic", + ], + ], + dependencies: [ + .project(target: "DesignKit", path: Path("../../Modules/DesignKit")), + .project(target: "BLEKit", path: Path("../../Modules/BLEKit")), + .project(target: "RobotKit", path: Path("../../Modules/RobotKit")), + .project(target: "LocalizationKit", path: Path("../../Modules/LocalizationKit")), + ] +) diff --git a/Apps/LekaUpdater/Resources/1-ACTIVITE REUSSIE.wav b/Apps/LekaUpdater/Resources/1-ACTIVITE REUSSIE.wav new file mode 100755 index 0000000000..1abc701eaf --- /dev/null +++ b/Apps/LekaUpdater/Resources/1-ACTIVITE REUSSIE.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0dda6803385d837c6c72794ff9381268fe8730eadbe5c16849ac9a33c93f559 +size 102358 diff --git a/Apps/LekaUpdater/Resources/21-TAG DETEC.wav b/Apps/LekaUpdater/Resources/21-TAG DETEC.wav new file mode 100755 index 0000000000..d06632bbeb --- /dev/null +++ b/Apps/LekaUpdater/Resources/21-TAG DETEC.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c0b24c95dd9fa0cf0c21915791c1223bb40990d3901560f78bc9859cc7164cb +size 41296 diff --git a/Apps/LekaUpdater/Resources/25-CONNEXION BT.wav b/Apps/LekaUpdater/Resources/25-CONNEXION BT.wav new file mode 100755 index 0000000000..c684b53218 --- /dev/null +++ b/Apps/LekaUpdater/Resources/25-CONNEXION BT.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5ceb591fe84f4eeff85c39eb00345c5490cac9c99ea23e857717e3f4405f4f3 +size 204126 diff --git a/Apps/LekaUpdater/Resources/27-BATTERIE MINIME.wav b/Apps/LekaUpdater/Resources/27-BATTERIE MINIME.wav new file mode 100755 index 0000000000..6fb7116419 --- /dev/null +++ b/Apps/LekaUpdater/Resources/27-BATTERIE MINIME.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c664a9ad26352e7cac25109856ec96033f1b2fe3921779f4c90eb7fede60ff6f +size 122712 diff --git a/Apps/LekaUpdater/Resources/3-BATTERIE FAIBLE.wav b/Apps/LekaUpdater/Resources/3-BATTERIE FAIBLE.wav new file mode 100755 index 0000000000..b0b80cb025 --- /dev/null +++ b/Apps/LekaUpdater/Resources/3-BATTERIE FAIBLE.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:548961904460277591265df6f65ea2488f11deacf3ca6c246e4702a443f49613 +size 224844 diff --git a/Apps/LekaUpdater/Resources/37-WELCOME.wav b/Apps/LekaUpdater/Resources/37-WELCOME.wav new file mode 100755 index 0000000000..248017ce0f --- /dev/null +++ b/Apps/LekaUpdater/Resources/37-WELCOME.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:caceb4bdcc612d6e7156b2e400b65d2742658b903be33a4d031d73e1eeebf2fc +size 743090 diff --git a/Apps/LekaUpdater/Resources/8-FIN DE CHARGE.wav b/Apps/LekaUpdater/Resources/8-FIN DE CHARGE.wav new file mode 100755 index 0000000000..381acc98c1 --- /dev/null +++ b/Apps/LekaUpdater/Resources/8-FIN DE CHARGE.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60e0bf7fd8c8bcf696fa4bebfe8b6d8e5ad8b05012058157d05ada2303b1ce51 +size 327402 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Apps/LekaUpdater/Resources/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897008..1f23a6afa4 100644 --- a/Apps/LekaUpdater/Resources/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,15 @@ { "colors" : [ { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0x9B", + "green" : "0x57", + "red" : "0x0A" + } + }, "idiom" : "universal" } ], diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/100.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 0000000000..b0d707d69a --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:864ebd3a665885e568ade639f8513178e12321137b5574c08f902011e550d7bf +size 9303 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/1024.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000000..54e495348f --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/1024.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:395258f3e5b22244517218d4e8ca3877fc5816eb92aa74f72cb503d8557a3a67 +size 139424 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/114.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 0000000000..f9101a3184 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/114.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fff07c97fa4571fae53fd5d05219a8bc948fec1c5e2339ab0340203d673ef98a +size 10802 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/120.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000000..68ff8f2fd1 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/120.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce6f944b2bd543a4080bd535ee29b02f97bc22ae69d2466216bb2279db8714ec +size 11385 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/128.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/128.png new file mode 100644 index 0000000000..4e1c4f33d8 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/128.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91cd3f572fa9ac085d2466858e819b77c45d3f2e6d251f073ddb64704ea98ffa +size 12275 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/144.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/144.png new file mode 100644 index 0000000000..c992bb0a02 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/144.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cdfd4699f4a6239b329e817acbec693048d1d0aff55f7db68c1deb163fee5ad +size 14128 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/152.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 0000000000..d0204ae91b --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/152.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70ed4e526d40759655bb6a021ad820cddaead51bceafa9e9c7dde241115a8439 +size 14998 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/16.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/16.png new file mode 100644 index 0000000000..20cf5ca440 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/16.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08a0c4af39b148bfc85a7eb1c049778a69b51bd6f8a74da40001f7f86d7f4808 +size 803 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/167.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 0000000000..8efda034e4 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/167.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aec73461ad50d0815aaa6ff5aaf59fdc7d81da565caf1bc95234ea294d18e545 +size 16688 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/180.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 0000000000..416161890e --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/180.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:152a4ec8816fd815723194bf82da2d00fb99e7ee765ced7f3726a2df5355aa41 +size 18422 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/20.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 0000000000..7e42b707c0 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1875b7ea12473f0bb4ce9fa7b3d0cfa586ad4116e990c05652f68e5db3583766 +size 1113 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/256.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/256.png new file mode 100644 index 0000000000..bec15ca04a --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/256.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f14b5d0e6e5ac5cebb881b20044583a6a8dbeff289577e83d60853aa0e245fef +size 27920 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/29.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 0000000000..46fb0e340c --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/29.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:324f3c3ed7bdc4529db110184346ce6d8a28d0033b299e9537e411e0a2077ccf +size 1830 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/32.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/32.png new file mode 100644 index 0000000000..0d2c305a04 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc901da3b429a8590f6a778157041c48c819161db985014a05f02d337aaad6aa +size 2053 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/40.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000000..3804075373 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/40.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e4fd9f9812e56c342aec8b06ace2742263656805309c9f67a16476735e85e7c +size 3094 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/50.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/50.png new file mode 100644 index 0000000000..b6bc90d979 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/50.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b10c5f471da9a8ca6fef1775226cb24df6284c422482c5bbb54c79b2de0402f +size 4046 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/512.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/512.png new file mode 100644 index 0000000000..8ff165aa6b --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/512.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c4b64cfe5323a862252d18361b2648e2b6f0387f38b62bda250842204595d7a +size 60999 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/57.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 0000000000..c2234391c2 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/57.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c242503a89ad30c9984f8cb66c1361fca1338e2d7aadcda859ad1103ce78b5b +size 4787 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/58.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 0000000000..cafa165174 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/58.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c4b4a45795dc6006106faa36be681eeeca24b00af511a4b242669014867fd2a +size 4921 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/60.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000000..87e612f975 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/60.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf7c7266d98c71708c27b425f57ab924afa54847a7054e9ab26ec0cac4de32a4 +size 5110 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/64.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/64.png new file mode 100644 index 0000000000..94e13c5537 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/64.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f961e6d1c1d85326846b33ea2fa79b1cf934e768308335755526bfdb44a44bef +size 5438 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/72.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/72.png new file mode 100644 index 0000000000..3f3fa6f8a2 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/72.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1dc97bcae33cda3fd135793345fcde4c5f14cd837d2cd033685d2b7549480746 +size 6343 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/76.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 0000000000..a3bc26f677 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/76.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e169a3852b15bd961310fd4a0f52ea4ef72bcbfe9768bce481282c1e47dc2b6 +size 6729 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/80.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000000..03470d4676 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/80.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0b27774d885faecd00bcef7598cd0ac9fd1573908496b441c1fd2997ae0fee5 +size 7161 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/87.png b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 0000000000..f231d9f16c --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/87.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8f675e18a774648951ed76b0da26e825ee1e9f9e7e45678fc3102a0a8cdd7b5 +size 7925 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 9221b9bb1a..bbf91d18f8 100644 --- a/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,94 +1,214 @@ { "images" : [ { + "filename" : "40.png", "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { + "filename" : "60.png", "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { + "filename" : "29.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58.png", "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { + "filename" : "87.png", "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { + "filename" : "80.png", "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { + "filename" : "120.png", "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { + "filename" : "57.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "57x57" + }, + { + "filename" : "114.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "57x57" + }, + { + "filename" : "120.png", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { + "filename" : "180.png", "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { + "filename" : "20.png", "idiom" : "ipad", "scale" : "1x", "size" : "20x20" }, { + "filename" : "40.png", "idiom" : "ipad", "scale" : "2x", "size" : "20x20" }, { + "filename" : "29.png", "idiom" : "ipad", "scale" : "1x", "size" : "29x29" }, { + "filename" : "58.png", "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { + "filename" : "40.png", "idiom" : "ipad", "scale" : "1x", "size" : "40x40" }, { + "filename" : "80.png", "idiom" : "ipad", "scale" : "2x", "size" : "40x40" }, { + "filename" : "50.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "50x50" + }, + { + "filename" : "100.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "50x50" + }, + { + "filename" : "72.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "72x72" + }, + { + "filename" : "144.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "72x72" + }, + { + "filename" : "76.png", "idiom" : "ipad", "scale" : "1x", "size" : "76x76" }, { + "filename" : "152.png", "idiom" : "ipad", "scale" : "2x", "size" : "76x76" }, { + "filename" : "167.png", "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" }, { + "filename" : "1024.png", "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" + }, + { + "filename" : "16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "32.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "64.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "256.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "512.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "1024.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" } ], "info" : { diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/ChargingBaseGreenLED.imageset/ChargingBaseGreenLED.svg b/Apps/LekaUpdater/Resources/Assets.xcassets/ChargingBaseGreenLED.imageset/ChargingBaseGreenLED.svg new file mode 100644 index 0000000000..0b7bdf8f51 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/ChargingBaseGreenLED.imageset/ChargingBaseGreenLED.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9649e7ba28b5eb3cb53f755acea333136d867b141e0a210ad9c2d1916ed9e673 +size 4406 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/ChargingBaseGreenLED.imageset/Contents.json b/Apps/LekaUpdater/Resources/Assets.xcassets/ChargingBaseGreenLED.imageset/Contents.json new file mode 100644 index 0000000000..b8535c4adf --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/ChargingBaseGreenLED.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ChargingBaseGreenLED.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/ChargingBasePlugged.imageset/ChargingBasePlugged.svg b/Apps/LekaUpdater/Resources/Assets.xcassets/ChargingBasePlugged.imageset/ChargingBasePlugged.svg new file mode 100644 index 0000000000..3b60a7a34b --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/ChargingBasePlugged.imageset/ChargingBasePlugged.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36dc4ea1e4b641ba2cf859d434b6d89b89dde452f1d6bacda4b0c0c24fee98b3 +size 10148 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/ChargingBasePlugged.imageset/Contents.json b/Apps/LekaUpdater/Resources/Assets.xcassets/ChargingBasePlugged.imageset/Contents.json new file mode 100644 index 0000000000..67f6132f35 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/ChargingBasePlugged.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ChargingBasePlugged.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/LekaUpdaterIcon.imageset/Contents.json b/Apps/LekaUpdater/Resources/Assets.xcassets/LekaUpdaterIcon.imageset/Contents.json new file mode 100644 index 0000000000..d37ed46396 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/LekaUpdaterIcon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "LekaUpdaterIcon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/LekaUpdaterIcon.imageset/LekaUpdaterIcon.svg b/Apps/LekaUpdater/Resources/Assets.xcassets/LekaUpdaterIcon.imageset/LekaUpdaterIcon.svg new file mode 100644 index 0000000000..9ab46f8874 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/LekaUpdaterIcon.imageset/LekaUpdaterIcon.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59bd1628e32eea74d37cb8e42d4410e49fcaa325eef594cbe83b77fd48e0b080 +size 3880 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/RobotBatteryQuarter1.imageset/Contents.json b/Apps/LekaUpdater/Resources/Assets.xcassets/RobotBatteryQuarter1.imageset/Contents.json new file mode 100644 index 0000000000..5c65d4d24b --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/RobotBatteryQuarter1.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "RobotBatteryQuarter1.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/RobotBatteryQuarter1.imageset/RobotBatteryQuarter1.svg b/Apps/LekaUpdater/Resources/Assets.xcassets/RobotBatteryQuarter1.imageset/RobotBatteryQuarter1.svg new file mode 100644 index 0000000000..feb8ba629c --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/RobotBatteryQuarter1.imageset/RobotBatteryQuarter1.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:058496d9c722d46932e46a204c2cca1408796f00c10aef995e6c9331df653445 +size 6642 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/RobotOnBase.imageset/Contents.json b/Apps/LekaUpdater/Resources/Assets.xcassets/RobotOnBase.imageset/Contents.json new file mode 100644 index 0000000000..c740a7d7de --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/RobotOnBase.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "RobotOnBase.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/RobotOnBase.imageset/RobotOnBase.svg b/Apps/LekaUpdater/Resources/Assets.xcassets/RobotOnBase.imageset/RobotOnBase.svg new file mode 100644 index 0000000000..cfc7198886 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/RobotOnBase.imageset/RobotOnBase.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfc9ddb2115939273f834e7aa94c47dc60e0c61344c731037bdfcd8533a1be2c +size 24163 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/SendingUpdate.imageset/Contents.json b/Apps/LekaUpdater/Resources/Assets.xcassets/SendingUpdate.imageset/Contents.json new file mode 100644 index 0000000000..fb03bca34e --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/SendingUpdate.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "SendingUpdate.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/SendingUpdate.imageset/SendingUpdate.svg b/Apps/LekaUpdater/Resources/Assets.xcassets/SendingUpdate.imageset/SendingUpdate.svg new file mode 100644 index 0000000000..c0a8990170 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/SendingUpdate.imageset/SendingUpdate.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f44e4c4f1ee89a202333e671d6e298a8d1e42169caa82f643058ab5ffdb86049 +size 23313 diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/UpdateInstallation.imageset/Contents.json b/Apps/LekaUpdater/Resources/Assets.xcassets/UpdateInstallation.imageset/Contents.json new file mode 100644 index 0000000000..49f26de52f --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/UpdateInstallation.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "UpdateInstallation.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Apps/LekaUpdater/Resources/Assets.xcassets/UpdateInstallation.imageset/UpdateInstallation.svg b/Apps/LekaUpdater/Resources/Assets.xcassets/UpdateInstallation.imageset/UpdateInstallation.svg new file mode 100644 index 0000000000..abef1fe6c5 --- /dev/null +++ b/Apps/LekaUpdater/Resources/Assets.xcassets/UpdateInstallation.imageset/UpdateInstallation.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5db835bf91d511fac70e2aaf31a1077dd528cef2408e3bf9c03a0c1600c62df5 +size 37921 diff --git a/Apps/LekaUpdater/Resources/LaunchScreen.logo.png b/Apps/LekaUpdater/Resources/LaunchScreen.logo.png new file mode 100644 index 0000000000..9f475b3240 --- /dev/null +++ b/Apps/LekaUpdater/Resources/LaunchScreen.logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0418a8589a82f989125883d19509760ce05364f4b6d81dd14dbfdcf6749576fe +size 31872 diff --git a/Apps/LekaUpdater/Resources/LaunchScreen.storyboard b/Apps/LekaUpdater/Resources/LaunchScreen.storyboard new file mode 100644 index 0000000000..a7b7feb11d --- /dev/null +++ b/Apps/LekaUpdater/Resources/LaunchScreen.storyboard @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Apps/LekaUpdater/Resources/LekaOS-1.4.0-en.md b/Apps/LekaUpdater/Resources/LekaOS-1.4.0-en.md new file mode 100644 index 0000000000..d34a5c8dee --- /dev/null +++ b/Apps/LekaUpdater/Resources/LekaOS-1.4.0-en.md @@ -0,0 +1,6 @@ +**Release Highlights** + +- ⚡️ Enhanced Stability: The robot now boasts improved stability during motion, ensuring smoother transitions and steadier movements. +- ⚡️ Precision in Rotations: We've fine-tuned the robot's rotational movements for greater accuracy and fluidity. +- 🌐 Language Support in Autonomous Mode: When the "Dice" card is recognized, the autonomous activities menu is now conveniently displayed in English. +- 🐛 Inactivity Timeout: To conserve energy and enhance safety, the robot will automatically exit autonomous mode after 10 minutes of inactivity. diff --git a/Apps/LekaUpdater/Resources/LekaOS-1.4.0-fr.md b/Apps/LekaUpdater/Resources/LekaOS-1.4.0-fr.md new file mode 100644 index 0000000000..6ffe63add9 --- /dev/null +++ b/Apps/LekaUpdater/Resources/LekaOS-1.4.0-fr.md @@ -0,0 +1,6 @@ +**Points Forts de la Mise à Jour** + +- ⚡️ Stabilité Améliorée : Le robot bénéficie désormais d'une stabilité accrue en mouvement, assurant des transitions plus douces et des déplacements plus stables. +- ⚡️ Précision des Rotations : Nous avons peaufiné les mouvements rotationnels du robot pour une plus grande précision et fluidité. +- 🌐 Support Linguistique en Mode Autonome : Lorsque la carte "Dice" est reconnue, le menu des activités autonomes s'affiche désormais commodément en anglais. +- 🐛 Délai d'Inactivité : Afin de conserver l'énergie et d'améliorer la sécurité, le robot sortira automatiquement du mode autonome après 10 minutes d'inactivité. diff --git a/Apps/LekaUpdater/Resources/LekaOS-1.4.0.bin b/Apps/LekaUpdater/Resources/LekaOS-1.4.0.bin new file mode 100644 index 0000000000..76d65242e4 --- /dev/null +++ b/Apps/LekaUpdater/Resources/LekaOS-1.4.0.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e061ee13041fe73667e4e9ca8f84189fb4cbc8f3d8710f8841fb41cf0df9e1e1 +size 505948 diff --git a/Apps/LekaUpdater/Resources/LekaOS-1.4.100-en.md b/Apps/LekaUpdater/Resources/LekaOS-1.4.100-en.md new file mode 100644 index 0000000000..d34a5c8dee --- /dev/null +++ b/Apps/LekaUpdater/Resources/LekaOS-1.4.100-en.md @@ -0,0 +1,6 @@ +**Release Highlights** + +- ⚡️ Enhanced Stability: The robot now boasts improved stability during motion, ensuring smoother transitions and steadier movements. +- ⚡️ Precision in Rotations: We've fine-tuned the robot's rotational movements for greater accuracy and fluidity. +- 🌐 Language Support in Autonomous Mode: When the "Dice" card is recognized, the autonomous activities menu is now conveniently displayed in English. +- 🐛 Inactivity Timeout: To conserve energy and enhance safety, the robot will automatically exit autonomous mode after 10 minutes of inactivity. diff --git a/Apps/LekaUpdater/Resources/LekaOS-1.4.100-fr.md b/Apps/LekaUpdater/Resources/LekaOS-1.4.100-fr.md new file mode 100644 index 0000000000..6ffe63add9 --- /dev/null +++ b/Apps/LekaUpdater/Resources/LekaOS-1.4.100-fr.md @@ -0,0 +1,6 @@ +**Points Forts de la Mise à Jour** + +- ⚡️ Stabilité Améliorée : Le robot bénéficie désormais d'une stabilité accrue en mouvement, assurant des transitions plus douces et des déplacements plus stables. +- ⚡️ Précision des Rotations : Nous avons peaufiné les mouvements rotationnels du robot pour une plus grande précision et fluidité. +- 🌐 Support Linguistique en Mode Autonome : Lorsque la carte "Dice" est reconnue, le menu des activités autonomes s'affiche désormais commodément en anglais. +- 🐛 Délai d'Inactivité : Afin de conserver l'énergie et d'améliorer la sécurité, le robot sortira automatiquement du mode autonome après 10 minutes d'inactivité. diff --git a/Apps/LekaUpdater/Resources/LekaOS-1.4.100.bin b/Apps/LekaUpdater/Resources/LekaOS-1.4.100.bin new file mode 100644 index 0000000000..7ef6ecdf44 --- /dev/null +++ b/Apps/LekaUpdater/Resources/LekaOS-1.4.100.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d679eba1475c43849e6d838a2f39187c746c7537c0ca76818b38bfdd0a9629fd +size 516872 diff --git a/Apps/LekaUpdater/Resources/LekaOS-1.4.101-en.md b/Apps/LekaUpdater/Resources/LekaOS-1.4.101-en.md new file mode 100644 index 0000000000..d34a5c8dee --- /dev/null +++ b/Apps/LekaUpdater/Resources/LekaOS-1.4.101-en.md @@ -0,0 +1,6 @@ +**Release Highlights** + +- ⚡️ Enhanced Stability: The robot now boasts improved stability during motion, ensuring smoother transitions and steadier movements. +- ⚡️ Precision in Rotations: We've fine-tuned the robot's rotational movements for greater accuracy and fluidity. +- 🌐 Language Support in Autonomous Mode: When the "Dice" card is recognized, the autonomous activities menu is now conveniently displayed in English. +- 🐛 Inactivity Timeout: To conserve energy and enhance safety, the robot will automatically exit autonomous mode after 10 minutes of inactivity. diff --git a/Apps/LekaUpdater/Resources/LekaOS-1.4.101-fr.md b/Apps/LekaUpdater/Resources/LekaOS-1.4.101-fr.md new file mode 100644 index 0000000000..6ffe63add9 --- /dev/null +++ b/Apps/LekaUpdater/Resources/LekaOS-1.4.101-fr.md @@ -0,0 +1,6 @@ +**Points Forts de la Mise à Jour** + +- ⚡️ Stabilité Améliorée : Le robot bénéficie désormais d'une stabilité accrue en mouvement, assurant des transitions plus douces et des déplacements plus stables. +- ⚡️ Précision des Rotations : Nous avons peaufiné les mouvements rotationnels du robot pour une plus grande précision et fluidité. +- 🌐 Support Linguistique en Mode Autonome : Lorsque la carte "Dice" est reconnue, le menu des activités autonomes s'affiche désormais commodément en anglais. +- 🐛 Délai d'Inactivité : Afin de conserver l'énergie et d'améliorer la sécurité, le robot sortira automatiquement du mode autonome après 10 minutes d'inactivité. diff --git a/Apps/LekaUpdater/Resources/LekaOS-1.4.101.bin b/Apps/LekaUpdater/Resources/LekaOS-1.4.101.bin new file mode 100644 index 0000000000..f9263ebca7 --- /dev/null +++ b/Apps/LekaUpdater/Resources/LekaOS-1.4.101.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e182a0020b99b4830394437e64520ec7d94b82804a80a9a06820e6977a3dfb7e +size 516248 diff --git a/Apps/LekaUpdater/Resources/ll10n/Localizable.xcstrings b/Apps/LekaUpdater/Resources/ll10n/Localizable.xcstrings new file mode 100644 index 0000000000..59311f7518 --- /dev/null +++ b/Apps/LekaUpdater/Resources/ll10n/Localizable.xcstrings @@ -0,0 +1,904 @@ +{ + "version": "1.0", + "sourceLanguage": "en", + "strings": { + "connection.connect_button": { + "comment": "Connect button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Connect" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Se connecter" + } + } + } + }, + "connection.continue_button": { + "comment": "Continue button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Continue" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Continuer" + } + } + } + }, + "connection.disconnect_button": { + "comment": "Disconnect button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Disconnect" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Se d\u00e9connecter" + } + } + } + }, + "connection.no_robots_found_text": { + "comment": "No robount found text", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "No robots found..." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun robot trouv\u00e9..." + } + } + } + }, + "connection.robot_discovery_version": { + "comment": "Discovery version LekaOS v...", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "LekaOS v%@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "LekaOS v%@" + } + } + } + }, + "connection.search_button": { + "comment": "Search button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Search" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher" + } + } + } + }, + "connection.search_invite_text": { + "comment": "Search invite text", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Press the Search button to find robots around you" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lancer une recherche pour trouver les robots autour de vous !" + } + } + } + }, + "general.no": { + "comment": "No", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "No" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Non" + } + } + } + }, + "general.yes": { + "comment": "Yes", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Yes" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Oui" + } + } + } + }, + "information.changelog_not_found_text": { + "comment": "Changelog not found text", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Changelog is not available" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La liste des changements n'est pas disponbile" + } + } + } + }, + "information.changelog_section_title": { + "comment": "Changelog of latest firmware update", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Changelog" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Liste des changements apport\u00e9s" + } + } + } + }, + "information.robot.battery": { + "comment": "Connected robot battery level", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Battery: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Batterie : %@" + } + } + } + }, + "information.robot.charging_status": { + "comment": "Connected robot charging status", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Charging: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "En charge : %@" + } + } + } + }, + "information.robot.serial_number": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Serial Number: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Num\u00e9ro de s\u00e9rie : %@" + } + } + } + }, + "information.robot.version": { + "comment": "Connected robot version", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Version: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Version: %@" + } + } + } + }, + "information.start_update_button": { + "comment": "Start update button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Start robot update" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lancer la mise \u00e0 jour du robot" + } + } + } + }, + "information.status.alert.robot_not_in_charge": { + "comment": "Robot not in charge alert", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "\u26a0\ufe0f WARNING \u26a1\nThe robot is no longer in charge" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\u26a0\ufe0f ATTENTION \u26a1\nLe robot n'est plus en charge" + } + } + } + }, + "information.status.alert.robot_not_in_charge_button": { + "comment": "Robot not in charge alert button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Please put Leka back on its charging station and/or check it's plugged to a power outlet" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Veuillez reposer Leka sur sa base et/ou v\u00e9rifier le branchement" + } + } + } + }, + "information.status.robot_cannot_be_updated_text": { + "comment": "Robot cannot be updated text", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "\u26a0\ufe0f DEV \ud83d\udea7\nUpdate process not recognized or not available\n(Error code: #0003)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\u26a0\ufe0f DEV \ud83d\udea7\nProcessus de mise \u00e0 jour non reconnu ou inexistant\n(Code erreur #0003)" + } + } + } + }, + "information.status.robot_is_up_to_date": { + "comment": "Robot is up to date text", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "\ud83e\udd16 Your robot is up-to-date! \ud83c\udf89 You're all done \ud83d\udc4c" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\ud83e\udd16 Votre robot est \u00e0 jour ! \ud83c\udf89 Vous n'avez rien \u00e0 faire \ud83d\udc4c" + } + } + } + }, + "information.status.robot_update_available": { + "comment": "Robot firmware update available text", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "\u2b06\ufe0f New firmware update available \ud83d\udce6" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\u2b06\ufe0f Une mise \u00e0 jour est disponible \ud83d\udce6" + } + } + } + }, + "main.app_description": { + "comment": "Description of the application", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The app to update your Leka robots!" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L'application pour mettre \u00e0 jour vos robots Leka !" + } + } + } + }, + "main.app_name": { + "comment": "Name of the application", + "extractionState": "translated", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Leka Updater" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Leka Updater" + } + } + } + }, + "toolbar.connection_button": { + "comment": "Connection toolbar button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Connection" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connexion" + } + } + } + }, + "update.error": { + "comment": "Update error", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "An error has occurred" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une erreur s'est produite" + } + } + } + }, + "update.error.failed_to_load_file": { + "comment": "Failed to load file", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Robot update file cannot be opened\n(Error code #0001)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le fichier de mise \u00e0 jour du robot ne peut pas \u00eatre ouvert\n(Code erreur #0001)" + } + } + } + }, + "update.error.failed_to_load_file_instructions": { + "comment": "Failed to load file instructions", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Please reinstall the app" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Veuillez r\u00e9installer l'application" + } + } + } + }, + "update.error.robot_not_up_to_date": { + "comment": "Robot not up to date", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Update failed\n(Error code #0002)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Echec de la mise \u00e0 jour\n(Code erreur #0002)" + } + } + } + }, + "update.error.robot_not_up_to_date_instructions": { + "comment": "Robot not up to date instructions", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Reconnect the robot and restart the process" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Reconnectez le robot et relancez le processus" + } + } + } + }, + "update.error.robot_unexpected_disconnection": { + "comment": "Robot unexpected disconnection", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "The robot has been disconnected\n(Error code #0004)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le robot s'est d\u00e9connect\u00e9 de mani\u00e8re inattendue\n(Code erreur #0004)" + } + } + } + }, + "update.error.robot_unexpected_disconnection_instructions": { + "comment": "Robot unexpected disconnection instructions", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Restart the robot using the \"Emergency stop\" card,\nreconnect the robot and restart the process" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Red\u00e9marrer le robot \u00e0 l'aide de la carte \\\"Arr\u00eat d'urgence\\\",\nreconnectez le robot et relancez le processus" + } + } + } + }, + "update.error.unknown_error": { + "comment": "unknown error", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "An unknown error has occurred\n(Error code #0000)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une erreur inconnue s'est produite\n(Code erreur #0000)" + } + } + } + }, + "update.error.unknown_error_instructions": { + "comment": "unknown error instructions", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Please contact technical support" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Contactez le support technique" + } + } + } + }, + "update.error.update_process_not_available": { + "comment": "Update process not available", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Update process not recognized or not available\n(Error code #0003)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Processus de mise \u00e0 jour non disponible ou inconnu\n(Code erreur #0003)" + } + } + } + }, + "update.error.update_process_not_available_instructions": { + "comment": "Update process not available instructions", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Please contact technical support" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Contactez le support technique" + } + } + } + }, + "update.error_back_button": { + "comment": "Update error back button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Return to robot connection page" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Revenir \u00e0 la page de connexion" + } + } + } + }, + "update.error_call_to_action": { + "comment": "Update error call to action", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Contact technical support" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Contactez le support technique" + } + } + } + }, + "update.error_description": { + "comment": "Update error description", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Unknown error" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Erreur inconnue" + } + } + } + }, + "update.finished.launch_leka_app_button": { + "comment": "Launch Leka app button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Launch My Leka Alpha \ud83d\ude80" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lancer Mon Leka Alpha \ud83d\ude80" + } + } + } + }, + "update.finished.robot_updated_successfully": { + "comment": "Robot updated successfully", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Congrats! \ud83e\udd73\nYour robot is now up-to-date \ud83c\udf89" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bravo ! \ud83e\udd73\nVotre robot est maintenant \u00e0 jour \ud83c\udf89" + } + } + } + }, + "update.finished.update_another_robot_button": { + "comment": "Update another robot button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Update another robot" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mettre \u00e0 jour un autre robot" + } + } + } + }, + "update.rebooting.subtitle": { + "comment": "Rebooting subtitle", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Your robot will reboot in a few minutes" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre robot red\u00e9marrera dans quelques minutes" + } + } + } + }, + "update.rebooting.title": { + "comment": "Rebooting title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "The update is being installed" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Installation de la mise \u00e0 jour" + } + } + } + }, + "update.requirements.charging_base_green_led_text": { + "comment": "Base green LED must be on", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "The charging LED is green indicating the correct positioning on the base" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La LED de charge est verte indiquant le bon positionnement sur la base" + } + } + } + }, + "update.requirements.charging_base_plugged_text": { + "comment": "Base must be plugged", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "The robot is placed on its base and the base is connected to the mains" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le robot est pos\u00e9 sur son socle et le socle est branch\u00e9 au secteur" + } + } + } + }, + "update.requirements.instructions_text": { + "comment": "Requirements before update", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "To start the update, make sure that:" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pour d\u00e9marrer la mise \u00e0 jour, assurez-vous que :" + } + } + } + }, + "update.requirements.robot_battery_minimum_level_text": { + "comment": "Robot battery level must be at least 30%", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "The robot battery level is at least 30%" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le niveau de batterie du robot est d'au moins 30%" + } + } + } + }, + "update.sending.instruction": { + "comment": "Sending instruction", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Do not unplug your robot\nDo not remove it from its charging station\nDo not close the app" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ne d\u00e9branchez pas votre robot.\nNe le sortez pas de son socle de recharge.\nNe fermez pas l'application." + } + } + } + }, + "update.sending.title": { + "comment": "Sending title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Sending update to the robot" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Envoi de la mise \u00e0 jour vers le robot" + } + } + } + }, + "update.step_number": { + "comment": "Update step number", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Step %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\u00c9tape %@" + } + } + } + } + } +} diff --git a/Apps/LekaUpdater/Sources/LekaUpdaterApp.swift b/Apps/LekaUpdater/Sources/LekaUpdaterApp.swift deleted file mode 100644 index a46a016b47..0000000000 --- a/Apps/LekaUpdater/Sources/LekaUpdaterApp.swift +++ /dev/null @@ -1,11 +0,0 @@ -import SwiftUI -import CoreUI - -@main -struct LekaUpdaterApp: App { - var body: some Scene { - WindowGroup { - Hello("Leka Updater", in: .blue) - } - } -} diff --git a/Apps/LekaUpdater/Sources/Libs/FirmwareManager.swift b/Apps/LekaUpdater/Sources/Libs/FirmwareManager.swift new file mode 100644 index 0000000000..4a4f3df305 --- /dev/null +++ b/Apps/LekaUpdater/Sources/Libs/FirmwareManager.swift @@ -0,0 +1,68 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import CryptoKit +import Foundation +import Version + +// MARK: - RobotUpdateStatus + +enum RobotUpdateStatus { + case upToDate + case needsUpdate +} + +// MARK: - FirmwareManager + +class FirmwareManager: ObservableObject { + // MARK: Public + + @Published public var data = Data() + + public var major: UInt8 { + UInt8(self.currentVersion.major) + } + + public var minor: UInt8 { + UInt8(self.currentVersion.minor) + } + + public var revision: UInt16 { + UInt16(self.currentVersion.patch) + } + + public var sha256: String { + SHA256.hash(data: self.data).compactMap { String(format: "%02x", $0) }.joined() + } + + public func load() -> Bool { + guard let fileURL = Bundle.main.url(forResource: "LekaOS-\(currentVersion)", withExtension: "bin") else { + return false + } + + do { + self.data = try Data(contentsOf: fileURL) + return true + } catch { + return false + } + } + + // MARK: Internal + + // swiftlint:disable:next force_cast + let currentVersion = Version(Bundle.main.object(forInfoDictionaryKey: "LEKA_OS_VERSION") as! String)! + + func compareWith(version: Version?) -> RobotUpdateStatus { + guard let version else { + return .needsUpdate + } + + if version > self.currentVersion { + return .upToDate + } + + return .needsUpdate + } +} diff --git a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Template/UpdateProcessTemplate.swift b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Template/UpdateProcessTemplate.swift new file mode 100644 index 0000000000..ae057f644f --- /dev/null +++ b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Template/UpdateProcessTemplate.swift @@ -0,0 +1,71 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import Foundation + +class UpdateProcessTemplate: UpdateProcessProtocol { + // MARK: Lifecycle + + init() { + self.subscribeToStateUpdates() + } + + // MARK: Public + + // MARK: - Public variables + + public var currentStage = CurrentValueSubject(.initial) + public var sendingFileProgression = CurrentValueSubject(0.0) + + // MARK: Internal + + // MARK: - Internal states, events, errors + + enum UpdateState { + case initial + case inProgress + case success + } + + enum UpdateEvent { + case startUpdateRequested + } + + enum UpdateError: Error { + case unknown + case notAvailable + } + + func startProcess() { + self.currentInternalState.send(completion: .failure(.notAvailable)) + } + + // MARK: Private + + // MARK: - Private variables + + private var cancellables: Set = [] + private var currentInternalState = CurrentValueSubject(.initial) + + private func subscribeToStateUpdates() { + self.currentInternalState + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: self.convertCompletion, receiveValue: self.convertReceivedValue) + .store(in: &self.cancellables) + } + + private func convertCompletion(completion: Subscribers.Completion) { + switch completion { + case .finished: + self.currentStage.send(completion: .finished) + case let .failure(error): + self.currentStage.send(completion: .failure(.updateProcessNotAvailable)) // only available error + } + } + + private func convertReceivedValue(state _: UpdateState) { + self.currentStage.send(.initial) // only available state + } +} diff --git a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/UpdateProcessController.swift b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/UpdateProcessController.swift new file mode 100644 index 0000000000..905451a122 --- /dev/null +++ b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/UpdateProcessController.swift @@ -0,0 +1,89 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import Foundation +import RobotKit +import Version + +// MARK: - UpdateProcessStage + +enum UpdateProcessStage { + case initial + + // LekaOS 1.0.0+ + case sendingUpdate + case installingUpdate +} + +// MARK: - UpdateProcessError + +enum UpdateProcessError: Error { + case unknown + case updateProcessNotAvailable + + // LekaOS 1.0.0+ + case failedToLoadFile + case robotNotUpToDate + case robotUnexpectedDisconnection +} + +// MARK: - UpdateProcessController + +class UpdateProcessController { + // MARK: Lifecycle + + init() { + let currentRobotVersion = Robot.shared.osVersion.value + + switch currentRobotVersion { + case Version(1, 0, 0), + Version(1, 1, 0), + Version(1, 2, 0): + self.currentUpdateProcess = UpdateProcessV100() + case Version(1, 3, 0), + Version(1, 4, 0): + self.currentUpdateProcess = UpdateProcessV130() + case Version(1, 4, 100), + Version(1, 4, 101), + Version(1, 5, 0): + self.currentUpdateProcess = UpdateProcessV150() + default: + self.currentUpdateProcess = UpdateProcessTemplate() + } + + self.currentStage = self.currentUpdateProcess.currentStage + self.sendingFileProgression = self.currentUpdateProcess.sendingFileProgression + } + + // MARK: Public + + // MARK: - Public variables + + public static let availableVersions = [ + Version(1, 0, 0), + Version(1, 1, 0), + Version(1, 2, 0), + Version(1, 3, 0), + Version(1, 4, 0), + Version(1, 4, 100), + Version(1, 4, 101), + Version(1, 5, 0), + ] + + public var currentStage = CurrentValueSubject(.initial) + public var sendingFileProgression = CurrentValueSubject(0.0) + + // MARK: Internal + + func startUpdate() { + self.currentUpdateProcess.startProcess() + } + + // MARK: Private + + // MARK: - Private variables + + private var currentUpdateProcess: any UpdateProcessProtocol +} diff --git a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/UpdateProcessProtocol.swift b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/UpdateProcessProtocol.swift new file mode 100644 index 0000000000..7fe576e1fc --- /dev/null +++ b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/UpdateProcessProtocol.swift @@ -0,0 +1,13 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import Foundation + +protocol UpdateProcessProtocol { + var currentStage: CurrentValueSubject { get } + var sendingFileProgression: CurrentValueSubject { get } + + func startProcess() +} diff --git a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV100.swift b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV100.swift new file mode 100644 index 0000000000..be5099f6cc --- /dev/null +++ b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV100.swift @@ -0,0 +1,533 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import Combine +import Foundation +import GameplayKit +import RobotKit +import Version + +// MARK: - UpdateEvent + +// swiftlint:disable file_length + +private enum UpdateEvent { + case startUpdateRequested + + case fileLoaded + case failedToLoadFile + case destinationPathSet + case fileSent + case robotDisconnected + case robotDetected +} + +// MARK: - StateEventProcessor + +private protocol StateEventProcessor { + func process(event: UpdateEvent) +} + +// MARK: - StateInitial + +private class StateInitial: GKState, StateEventProcessor { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateLoadingUpdateFile.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + func process(event: UpdateEvent) { + switch event { + case .startUpdateRequested: + stateMachine?.enter(StateLoadingUpdateFile.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } +} + +// MARK: - StateLoadingUpdateFile + +private class StateLoadingUpdateFile: GKState, StateEventProcessor { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateErrorFailedToLoadFile.Type || stateClass is StateSettingDestinationPath.Type + || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + let isLoaded = globalFirmwareManager.load() + + if isLoaded { + self.process(event: .fileLoaded) + } else { + self.process(event: .failedToLoadFile) + } + } + + func process(event: UpdateEvent) { + switch event { + case .fileLoaded: + stateMachine?.enter(StateSettingDestinationPath.self) + case .failedToLoadFile: + stateMachine?.enter(StateErrorFailedToLoadFile.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } +} + +// MARK: - StateSettingDestinationPath + +private class StateSettingDestinationPath: GKState, StateEventProcessor { + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateSendingFile.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: self.setDestinationPath) + } + + func process(event: UpdateEvent) { + switch event { + case .destinationPathSet: + stateMachine?.enter(StateSendingFile.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private func setDestinationPath() { + let osVersion = globalFirmwareManager.currentVersion + + let directory = "/fs/usr/os" + let filename = "LekaOS-\(osVersion).bin" + let destinationPath = directory + "/" + filename + + let characteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.filePath, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + self.process(event: .destinationPathSet) + } + ) + + Robot.shared.connectedPeripheral?.send(destinationPath.data(using: .utf8)!, forCharacteristic: characteristic) + } +} + +// MARK: - StateSendingFile + +private class StateSendingFile: GKState, StateEventProcessor { + // MARK: Lifecycle + + override init() { + let dataSize = globalFirmwareManager.data.count + + self.expectedCompletePackets = Int(floor(Double(dataSize / self.maximumPacketSize))) + self.expectedRemainingBytes = Int(dataSize % self.maximumPacketSize) + + self.currentPacket = 0 + + super.init() + + self.subscribeToFirmwareDataUpdates() + } + + // MARK: Public + + public var progression = CurrentValueSubject(0.0) + + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateApplyingUpdate.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: self.sendFile) + } + + override func willExit(to _: GKState) { + self.cancellables.removeAll() + } + + func process(event: UpdateEvent) { + switch event { + case .fileSent: + stateMachine?.enter(StateApplyingUpdate.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private var cancellables: Set = [] + + private let maximumPacketSize: Int = 61 + + private var currentPacket: Int = 0 + private var expectedCompletePackets: Int + private var expectedRemainingBytes: Int + private lazy var characteristic: CharacteristicModelWriteOnly = .init( + characteristicUUID: BLESpecs.FileExchange.Characteristics.fileReceptionBuffer, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + self.currentPacket += 1 + self.tryToSendNextPacket() + } + ) + + private var expectedPackets: Int { + self.expectedRemainingBytes == 0 ? self.expectedCompletePackets : self.expectedCompletePackets + 1 + } + + private var _progression: Float { + Float(self.currentPacket) / Float(self.expectedPackets) + } + + private func subscribeToFirmwareDataUpdates() { + globalFirmwareManager.$data + .receive(on: DispatchQueue.main) + .sink { data in + let dataSize = data.count + + self.expectedCompletePackets = Int(floor(Double(dataSize / self.maximumPacketSize))) + self.expectedRemainingBytes = Int(dataSize % self.maximumPacketSize) + } + .store(in: &self.cancellables) + } + + private func sendFile() { + self.tryToSendNextPacket() + } + + private func tryToSendNextPacket() { + if self.isInCriticalSection() { + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: self.tryToSendNextPacket) + return + } + + self.progression.send(self._progression) + if self._progression < 1.0 { + self.sendNextPacket() + } else { + self.process(event: .fileSent) + } + } + + private func isInCriticalSection() -> Bool { + let isNotCharging = !Robot.shared.isCharging.value + + let battery = Robot.shared.battery.value + let isNearBatteryLevelChange = + 23...27 ~= battery || 48...52 ~= battery || 73...77 ~= battery || 88...92 ~= battery + + return isNotCharging || isNearBatteryLevelChange + } + + private func sendNextPacket() { + let startIndex = self.currentPacket * self.maximumPacketSize + let endIndex = + self.currentPacket < self.expectedCompletePackets + ? startIndex + self.maximumPacketSize - 1 : startIndex + self.expectedRemainingBytes - 1 + + let dataToSend = globalFirmwareManager.data[startIndex...endIndex] + + Robot.shared.connectedPeripheral?.send(dataToSend, forCharacteristic: self.characteristic) + } +} + +// MARK: - StateApplyingUpdate + +private class StateApplyingUpdate: GKState, StateEventProcessor { + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateWaitingForRobotToReboot.Type + } + + override func didEnter(from _: GKState?) { + self.setMajor() + } + + override func willExit(to _: GKState) { + self.cancellables.removeAll() + } + + func process(event: UpdateEvent) { + switch event { + case .robotDisconnected: + stateMachine?.enter(StateWaitingForRobotToReboot.self) + default: + return + } + } + + // MARK: Private + + private var cancellables: Set = [] + + private func setMajor() { + let majorData = Data([globalFirmwareManager.major]) + + let majorCharacteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FirmwareUpdate.Characteristics.versionMajor, + serviceUUID: BLESpecs.FirmwareUpdate.service, + onWrite: self.setMinor + ) + + Robot.shared.connectedPeripheral?.send(majorData, forCharacteristic: majorCharacteristic) + } + + private func setMinor() { + let minorData = Data([globalFirmwareManager.minor]) + + let minorCharacteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FirmwareUpdate.Characteristics.versionMinor, + serviceUUID: BLESpecs.FirmwareUpdate.service, + onWrite: self.setRevision + ) + + Robot.shared.connectedPeripheral?.send(minorData, forCharacteristic: minorCharacteristic) + } + + private func setRevision() { + let revisionData = globalFirmwareManager.revision.data + + let revisionCharacteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FirmwareUpdate.Characteristics.versionRevision, + serviceUUID: BLESpecs.FirmwareUpdate.service, + onWrite: self.applyUpdate + ) + + Robot.shared.connectedPeripheral?.send(revisionData, forCharacteristic: revisionCharacteristic) + } + + private func applyUpdate() { + let applyValue = Data([1]) + + let characteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FirmwareUpdate.Characteristics.requestUpdate, + serviceUUID: BLESpecs.FirmwareUpdate.service + ) + + Robot.shared.connectedPeripheral?.send(applyValue, forCharacteristic: characteristic) + } +} + +// MARK: - StateWaitingForRobotToReboot + +private class StateWaitingForRobotToReboot: GKState, StateEventProcessor { + // MARK: Lifecycle + + init(expectedRobot: RobotPeripheral?) { + self.expectedRobot = expectedRobot + } + + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateFinal.Type || stateClass is StateErrorRobotNotUpToDate.Type + || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + self.registerScanForRobot() + } + + override func willExit(to _: GKState) { + self.cancellables.removeAll() + } + + func process(event: UpdateEvent) { + switch event { + case .robotDetected: + if self.isRobotUpToDate { + stateMachine?.enter(StateFinal.self) + } else { + stateMachine?.enter(StateErrorRobotNotUpToDate.self) + } + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private var cancellables: Set = [] + + private var expectedRobot: RobotPeripheral? + private var isRobotUpToDate: Bool = false + + private func registerScanForRobot() { + BLEManager.shared.scanForRobots() + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { _ in + // nothing to do + }, + receiveValue: { robotDiscoveryList in + let robotDetected = robotDiscoveryList.first { robotDiscovery in + robotDiscovery.robotPeripheral == self.expectedRobot + } + if let robotDetected { + self.isRobotUpToDate = + Version(robotDetected.osVersion) == globalFirmwareManager.currentVersion + + self.process(event: .robotDetected) + } + } + ) + .store(in: &self.cancellables) + } +} + +// MARK: - StateFinal + +private class StateFinal: GKState {} + +// MARK: - StateError + +private protocol StateError {} + +// MARK: - StateErrorFailedToLoadFile + +private class StateErrorFailedToLoadFile: GKState, StateError {} + +// MARK: - StateErrorRobotNotUpToDate + +private class StateErrorRobotNotUpToDate: GKState, StateError {} + +// MARK: - StateErrorRobotUnexpectedDisconnection + +private class StateErrorRobotUnexpectedDisconnection: GKState, StateError {} + +// MARK: - UpdateProcessV100 + +class UpdateProcessV100: UpdateProcessProtocol { + // MARK: Lifecycle + + init() { + self.stateMachine = GKStateMachine(states: [ + StateInitial(), + + StateLoadingUpdateFile(), + self.stateSendingFile, + StateApplyingUpdate(), + StateWaitingForRobotToReboot(expectedRobot: Robot.shared.connectedPeripheral), + StateSettingDestinationPath(), + + StateFinal(), + + StateErrorFailedToLoadFile(), + StateErrorRobotNotUpToDate(), + StateErrorRobotUnexpectedDisconnection(), + ]) + self.stateMachine?.enter(StateInitial.self) + + self.startRoutineToUpdateCurrentState() + self.registerDidDisconnect() + self.sendingFileProgression = self.stateSendingFile.progression + } + + // MARK: Public + + // MARK: - Public variables + + public var currentStage = CurrentValueSubject(.initial) + public var sendingFileProgression = CurrentValueSubject(0.0) + + public func startProcess() { + self.process(event: .startUpdateRequested) + } + + // MARK: Private + + // MARK: - Private variables + + private var stateMachine: GKStateMachine? + private var stateSendingFile = StateSendingFile() + + private var cancellables: Set = [] + + private func process(event: UpdateEvent) { + guard let state = stateMachine?.currentState as? any StateEventProcessor else { + return + } + + state.process(event: event) + + self.updateCurrentState() + } + + private func registerDidDisconnect() { + BLEManager.shared.didDisconnect + .receive(on: DispatchQueue.main) + .sink { + self.process(event: .robotDisconnected) + } + .store(in: &self.cancellables) + } + + private func startRoutineToUpdateCurrentState() { + Timer.publish(every: 1, on: .main, in: .default) + .autoconnect() + .sink { _ in + self.updateCurrentState() + } + .store(in: &self.cancellables) + } + + private func updateCurrentState() { + guard let state = stateMachine?.currentState else { return } + + switch state { + case is StateInitial: + self.currentStage.send(.initial) + case is StateLoadingUpdateFile, + is StateSettingDestinationPath, + is StateSendingFile: + self.currentStage.send(.sendingUpdate) + case is StateApplyingUpdate, + is StateWaitingForRobotToReboot: + self.currentStage.send(.installingUpdate) + case is StateFinal: + self.currentStage.send(completion: .finished) + case is any StateError: + self.sendError(state: state) + default: + self.currentStage.send(completion: .failure(.unknown)) + } + } + + private func sendError(state: GKState) { + switch state { + case is StateErrorFailedToLoadFile: + self.currentStage.send(completion: .failure(.failedToLoadFile)) + case is StateErrorRobotNotUpToDate: + self.currentStage.send(completion: .failure(.robotNotUpToDate)) + case is StateErrorRobotUnexpectedDisconnection: + self.currentStage.send(completion: .failure(.robotUnexpectedDisconnection)) + default: + self.currentStage.send(completion: .failure(.unknown)) + } + } +} + +// swiftlint:enable file_length diff --git a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV130.swift b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV130.swift new file mode 100644 index 0000000000..5e9d4deeef --- /dev/null +++ b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV130.swift @@ -0,0 +1,611 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import Combine +import Foundation +import GameplayKit +import RobotKit +import Version + +// MARK: - UpdateEvent + +// swiftlint:disable file_length + +private enum UpdateEvent { + case startUpdateRequested + + case fileLoaded + case failedToLoadFile + case fileExchangeStateSet + case destinationPathSet + case fileCleared + case fileSent + case fileVerificationReceived + case robotDisconnected + case robotDetected +} + +// MARK: - StateEventProcessor + +private protocol StateEventProcessor { + func process(event: UpdateEvent) +} + +// MARK: - StateInitial + +private class StateInitial: GKState, StateEventProcessor { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateLoadingUpdateFile.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + func process(event: UpdateEvent) { + switch event { + case .startUpdateRequested: + stateMachine?.enter(StateLoadingUpdateFile.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } +} + +// MARK: - StateLoadingUpdateFile + +private class StateLoadingUpdateFile: GKState, StateEventProcessor { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateErrorFailedToLoadFile.Type || stateClass is StateSettingFileExchangeState.Type + || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + let isLoaded = globalFirmwareManager.load() + + if isLoaded { + self.process(event: .fileLoaded) + } else { + self.process(event: .failedToLoadFile) + } + } + + func process(event: UpdateEvent) { + switch event { + case .fileLoaded: + stateMachine?.enter(StateSettingFileExchangeState.self) + case .failedToLoadFile: + stateMachine?.enter(StateErrorFailedToLoadFile.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } +} + +// MARK: - StateSettingFileExchangeState + +private class StateSettingFileExchangeState: GKState, StateEventProcessor { + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateSettingDestinationPath.Type + || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: self.setFileExchangeState) + } + + func process(event: UpdateEvent) { + switch event { + case .fileExchangeStateSet: + stateMachine?.enter(StateSettingDestinationPath.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private func setFileExchangeState() { + let data = Data([1]) + + let characteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.setState, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + self.process(event: .fileExchangeStateSet) + } + ) + + Robot.shared.connectedPeripheral?.send(data, forCharacteristic: characteristic) + } +} + +// MARK: - StateSettingDestinationPath + +private class StateSettingDestinationPath: GKState, StateEventProcessor { + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateClearingFile.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: self.setDestinationPath) + } + + func process(event: UpdateEvent) { + switch event { + case .destinationPathSet: + stateMachine?.enter(StateClearingFile.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private func setDestinationPath() { + let osVersion = globalFirmwareManager.currentVersion + + let directory = "/fs/usr/os" + let filename = "LekaOS-\(osVersion).bin" + let destinationPath = directory + "/" + filename + + let characteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.filePath, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + self.process(event: .destinationPathSet) + } + ) + + Robot.shared.connectedPeripheral?.send(destinationPath.data(using: .utf8)!, forCharacteristic: characteristic) + } +} + +// MARK: - StateClearingFile + +private class StateClearingFile: GKState, StateEventProcessor { + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateSendingFile.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: self.setClearPath) + } + + func process(event: UpdateEvent) { + switch event { + case .fileCleared: + stateMachine?.enter(StateSendingFile.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private func setClearPath() { + let data = Data([1]) + + let characteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.clearFile, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + self.process(event: .fileCleared) + } + ) + + Robot.shared.connectedPeripheral?.send(data, forCharacteristic: characteristic) + } +} + +// MARK: - StateSendingFile + +private class StateSendingFile: GKState, StateEventProcessor { + // MARK: Lifecycle + + override init() { + let dataSize = globalFirmwareManager.data.count + + self.expectedCompletePackets = Int(floor(Double(dataSize / self.maximumPacketSize))) + self.expectedRemainingBytes = Int(dataSize % self.maximumPacketSize) + + self.currentPacket = 0 + + super.init() + + self.subscribeToFirmwareDataUpdates() + } + + // MARK: Public + + public var progression = CurrentValueSubject(0.0) + + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateApplyingUpdate.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: self.sendFile) + } + + override func willExit(to _: GKState) { + self.cancellables.removeAll() + self.characteristic = nil + } + + func process(event: UpdateEvent) { + switch event { + case .fileSent: + stateMachine?.enter(StateApplyingUpdate.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private var cancellables: Set = [] + + private let maximumPacketSize: Int = 61 + + private var currentPacket: Int = 0 + private var expectedCompletePackets: Int + private var expectedRemainingBytes: Int + private lazy var characteristic: CharacteristicModelWriteOnly? = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.fileReceptionBuffer, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + self.currentPacket += 1 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.04, execute: self.tryToSendNextPacket) + } + ) + + private var expectedPackets: Int { + self.expectedRemainingBytes == 0 ? self.expectedCompletePackets : self.expectedCompletePackets + 1 + } + + private var _progression: Float { + Float(self.currentPacket) / Float(self.expectedPackets) + } + + private func subscribeToFirmwareDataUpdates() { + globalFirmwareManager.$data + .receive(on: DispatchQueue.main) + .sink { data in + let dataSize = data.count + + self.expectedCompletePackets = Int(floor(Double(dataSize / self.maximumPacketSize))) + self.expectedRemainingBytes = Int(dataSize % self.maximumPacketSize) + } + .store(in: &self.cancellables) + } + + private func sendFile() { + self.tryToSendNextPacket() + } + + private func tryToSendNextPacket() { + self.progression.send(self._progression) + if self._progression < 1.0 { + self.sendNextPacket() + } else { + self.process(event: .fileSent) + } + } + + private func sendNextPacket() { + let startIndex = self.currentPacket * self.maximumPacketSize + let endIndex = + self.currentPacket < self.expectedCompletePackets + ? startIndex + self.maximumPacketSize - 1 : startIndex + self.expectedRemainingBytes - 1 + + let dataToSend = globalFirmwareManager.data[startIndex...endIndex] + + if let characteristic { + Robot.shared.connectedPeripheral?.send(dataToSend, forCharacteristic: characteristic) + } + } +} + +// MARK: - StateApplyingUpdate + +private class StateApplyingUpdate: GKState, StateEventProcessor { + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateWaitingForRobotToReboot.Type + } + + override func didEnter(from _: GKState?) { + self.setMajor() + } + + override func willExit(to _: GKState) { + self.cancellables.removeAll() + } + + func process(event: UpdateEvent) { + switch event { + case .robotDisconnected: + stateMachine?.enter(StateWaitingForRobotToReboot.self) + default: + return + } + } + + // MARK: Private + + private var cancellables: Set = [] + + private func setMajor() { + let majorData = Data([globalFirmwareManager.major]) + + let majorCharacteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FirmwareUpdate.Characteristics.versionMajor, + serviceUUID: BLESpecs.FirmwareUpdate.service, + onWrite: self.setMinor + ) + + Robot.shared.connectedPeripheral?.send(majorData, forCharacteristic: majorCharacteristic) + } + + private func setMinor() { + let minorData = Data([globalFirmwareManager.minor]) + + let minorCharacteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FirmwareUpdate.Characteristics.versionMinor, + serviceUUID: BLESpecs.FirmwareUpdate.service, + onWrite: self.setRevision + ) + + Robot.shared.connectedPeripheral?.send(minorData, forCharacteristic: minorCharacteristic) + } + + private func setRevision() { + let revisionData = globalFirmwareManager.revision.data + + let revisionCharacteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FirmwareUpdate.Characteristics.versionRevision, + serviceUUID: BLESpecs.FirmwareUpdate.service, + onWrite: self.applyUpdate + ) + + Robot.shared.connectedPeripheral?.send(revisionData, forCharacteristic: revisionCharacteristic) + } + + private func applyUpdate() { + let applyValue = Data([1]) + + let characteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FirmwareUpdate.Characteristics.requestUpdate, + serviceUUID: BLESpecs.FirmwareUpdate.service + ) + + Robot.shared.connectedPeripheral?.send(applyValue, forCharacteristic: characteristic) + } +} + +// MARK: - StateWaitingForRobotToReboot + +private class StateWaitingForRobotToReboot: GKState, StateEventProcessor { + // MARK: Lifecycle + + init(expectedRobot: RobotPeripheral?) { + self.expectedRobot = expectedRobot + } + + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateFinal.Type || stateClass is StateErrorRobotNotUpToDate.Type + || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + self.registerScanForRobot() + } + + override func willExit(to _: GKState) { + self.cancellables.removeAll() + } + + func process(event: UpdateEvent) { + switch event { + case .robotDetected: + if self.isRobotUpToDate { + stateMachine?.enter(StateFinal.self) + } else { + stateMachine?.enter(StateErrorRobotNotUpToDate.self) + } + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private var cancellables: Set = [] + + private var expectedRobot: RobotPeripheral? + private var isRobotUpToDate: Bool = false + + private func registerScanForRobot() { + BLEManager.shared.scanForRobots() + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { _ in + // nothing to do + }, + receiveValue: { robotDiscoveryList in + let robotDetected = robotDiscoveryList.first { robotDiscovery in + robotDiscovery.robotPeripheral == self.expectedRobot + } + if let robotDetected { + self.isRobotUpToDate = + Version(robotDetected.osVersion) == globalFirmwareManager.currentVersion + + self.process(event: .robotDetected) + } + } + ) + .store(in: &self.cancellables) + } +} + +// MARK: - StateFinal + +private class StateFinal: GKState {} + +// MARK: - StateError + +private protocol StateError {} + +// MARK: - StateErrorFailedToLoadFile + +private class StateErrorFailedToLoadFile: GKState, StateError {} + +// MARK: - StateErrorRobotNotUpToDate + +private class StateErrorRobotNotUpToDate: GKState, StateError {} + +// MARK: - StateErrorRobotUnexpectedDisconnection + +private class StateErrorRobotUnexpectedDisconnection: GKState, StateError {} + +// MARK: - UpdateProcessV130 + +class UpdateProcessV130: UpdateProcessProtocol { + // MARK: Lifecycle + + init() { + self.stateMachine = GKStateMachine(states: [ + StateInitial(), + + StateLoadingUpdateFile(), + StateSettingFileExchangeState(), + StateSettingDestinationPath(), + StateClearingFile(), + self.stateSendingFile, + StateApplyingUpdate(), + StateWaitingForRobotToReboot(expectedRobot: Robot.shared.connectedPeripheral), + + StateFinal(), + + StateErrorFailedToLoadFile(), + StateErrorRobotNotUpToDate(), + StateErrorRobotUnexpectedDisconnection(), + ]) + self.stateMachine?.enter(StateInitial.self) + + self.startRoutineToUpdateCurrentState() + self.registerDidDisconnect() + self.sendingFileProgression = self.stateSendingFile.progression + } + + // MARK: Public + + // MARK: - Public variables + + public var currentStage = CurrentValueSubject(.initial) + public var sendingFileProgression = CurrentValueSubject(0.0) + + public func startProcess() { + self.process(event: .startUpdateRequested) + } + + // MARK: Private + + // MARK: - Private variables + + private var stateMachine: GKStateMachine? + private var stateSendingFile = StateSendingFile() + + private var cancellables: Set = [] + + private func process(event: UpdateEvent) { + guard let state = stateMachine?.currentState as? any StateEventProcessor else { + return + } + + state.process(event: event) + + self.updateCurrentState() + } + + private func registerDidDisconnect() { + BLEManager.shared.didDisconnect + .receive(on: DispatchQueue.main) + .sink { + self.process(event: .robotDisconnected) + } + .store(in: &self.cancellables) + } + + private func startRoutineToUpdateCurrentState() { + Timer.publish(every: 1, on: .main, in: .default) + .autoconnect() + .sink { _ in + self.updateCurrentState() + } + .store(in: &self.cancellables) + } + + private func updateCurrentState() { + guard let state = stateMachine?.currentState else { return } + + switch state { + case is StateInitial: + self.currentStage.send(.initial) + case is StateLoadingUpdateFile, + is StateSettingFileExchangeState, + is StateSettingDestinationPath, + is StateClearingFile, + is StateSendingFile: + self.currentStage.send(.sendingUpdate) + case is StateApplyingUpdate, + is StateWaitingForRobotToReboot: + self.currentStage.send(.installingUpdate) + case is StateFinal: + self.currentStage.send(completion: .finished) + case is any StateError: + self.sendError(state: state) + default: + self.currentStage.send(completion: .failure(.unknown)) + } + } + + private func sendError(state: GKState) { + switch state { + case is StateErrorFailedToLoadFile: + self.currentStage.send(completion: .failure(.failedToLoadFile)) + case is StateErrorRobotNotUpToDate: + self.currentStage.send(completion: .failure(.robotNotUpToDate)) + case is StateErrorRobotUnexpectedDisconnection: + self.currentStage.send(completion: .failure(.robotUnexpectedDisconnection)) + default: + self.currentStage.send(completion: .failure(.unknown)) + } + } +} + +// swiftlint:enable file_length diff --git a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV150.swift b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV150.swift new file mode 100644 index 0000000000..8442286974 --- /dev/null +++ b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV150.swift @@ -0,0 +1,808 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import Combine +import Foundation +import GameplayKit +import RobotKit +import Version + +// MARK: - UpdateEvent + +// swiftlint:disable file_length + +private enum UpdateEvent { + case startUpdateRequested + + case fileLoaded + case failedToLoadFile + case fileExchangeStateSet + case destinationPathSet + case fileCleared + case fileSent + case assetsSent + case fileVerificationReceived + case robotDisconnected + case robotDetected +} + +// MARK: - StateEventProcessor + +private protocol StateEventProcessor { + func process(event: UpdateEvent) +} + +// MARK: - StateInitial + +private class StateInitial: GKState, StateEventProcessor { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateLoadingUpdateFile.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + func process(event: UpdateEvent) { + switch event { + case .startUpdateRequested: + stateMachine?.enter(StateLoadingUpdateFile.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } +} + +// MARK: - StateLoadingUpdateFile + +private class StateLoadingUpdateFile: GKState, StateEventProcessor { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateErrorFailedToLoadFile.Type || stateClass is StateSettingFileExchangeState.Type + || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + let isLoaded = globalFirmwareManager.load() + + if isLoaded { + self.process(event: .fileLoaded) + } else { + self.process(event: .failedToLoadFile) + } + } + + func process(event: UpdateEvent) { + switch event { + case .fileLoaded: + stateMachine?.enter(StateSettingFileExchangeState.self) + case .failedToLoadFile: + stateMachine?.enter(StateErrorFailedToLoadFile.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } +} + +// MARK: - StateSettingFileExchangeState + +private class StateSettingFileExchangeState: GKState, StateEventProcessor { + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateSettingDestinationPath.Type + || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + self.setFileExchangeState() + } + + func process(event: UpdateEvent) { + switch event { + case .fileExchangeStateSet: + stateMachine?.enter(StateSettingDestinationPath.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private func setFileExchangeState() { + let data = Data([1]) + + let characteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.setState, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + sleep(1) + self.process(event: .fileExchangeStateSet) + } + ) + + Robot.shared.connectedPeripheral?.send(data, forCharacteristic: characteristic) + } +} + +// MARK: - StateSettingDestinationPath + +private class StateSettingDestinationPath: GKState, StateEventProcessor { + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateClearingFile.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + if Robot.shared.osVersion.value! == globalFirmwareManager.currentVersion{ + process(event: .destinationPathSet) + return + } + + self.setDestinationPath() + } + + func process(event: UpdateEvent) { + switch event { + case .destinationPathSet: + stateMachine?.enter(StateClearingFile.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private func setDestinationPath() { + let osVersion = globalFirmwareManager.currentVersion + + let directory = "/fs/usr/os" + let filename = "LekaOS-\(osVersion).bin" + let destinationPath = directory + "/" + filename + + let characteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.filePath, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + self.process(event: .destinationPathSet) + } + ) + + Robot.shared.connectedPeripheral?.send(destinationPath.data(using: .utf8)!, forCharacteristic: characteristic) + } +} + +// MARK: - StateClearingFile + +private class StateClearingFile: GKState, StateEventProcessor { + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateSendingFile.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + if Robot.shared.osVersion.value! == globalFirmwareManager.currentVersion{ + process(event: .fileCleared) + return + } + + self.setClearPath() + } + + func process(event: UpdateEvent) { + switch event { + case .fileCleared: + stateMachine?.enter(StateSendingFile.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private func setClearPath() { + let data = Data([1]) + + let characteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.clearFile, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + self.process(event: .fileCleared) + } + ) + + Robot.shared.connectedPeripheral?.send(data, forCharacteristic: characteristic) + } +} + +// MARK: - StateSendingFile + +private class StateSendingFile: GKState, StateEventProcessor { + // MARK: Lifecycle + + override init() { + let dataSize = globalFirmwareManager.data.count + + self.expectedCompletePackets = Int(floor(Double(dataSize / self.maximumPacketSize))) + self.expectedRemainingBytes = Int(dataSize % self.maximumPacketSize) + + self.currentPacket = 0 + + super.init() + + self.subscribeToFirmwareDataUpdates() + } + + // MARK: Public + + public var progression = CurrentValueSubject(0.0) + + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateSendingAssets.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + if Robot.shared.osVersion.value! == globalFirmwareManager.currentVersion{ + process(event: .fileSent) + return + } + + self.sendFile() + } + + override func willExit(to _: GKState) { + self.cancellables.removeAll() + self.characteristic = nil + } + + func process(event: UpdateEvent) { + switch event { + case .fileSent: + debugPrint("Robot version is \(Robot.shared.osVersion.value!)") + if Robot.shared.osVersion.value! >= Version("1.4.100")! { + debugPrint("Send assets") + stateMachine?.enter(StateSendingAssets.self) + } else { + debugPrint("Apply update") + stateMachine?.enter(StateApplyingUpdate.self) + } + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private var cancellables: Set = [] + + private let maximumPacketSize: Int = 182 // MTU(max) - 3 + + private var currentPacket: Int = 0 + private var expectedCompletePackets: Int + private var expectedRemainingBytes: Int + private lazy var characteristic: CharacteristicModelWriteOnly? = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.fileReceptionBuffer, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + self.currentPacket += 1 + self.tryToSendNextPacket() + } + ) + + private var expectedPackets: Int { + self.expectedRemainingBytes == 0 ? self.expectedCompletePackets : self.expectedCompletePackets + 1 + } + + private var _progression: Float { + Float(self.currentPacket) / Float(self.expectedPackets) + } + + private func subscribeToFirmwareDataUpdates() { + globalFirmwareManager.$data + .receive(on: DispatchQueue.main) + .sink { data in + let dataSize = data.count + + self.expectedCompletePackets = Int(floor(Double(dataSize / self.maximumPacketSize))) + self.expectedRemainingBytes = Int(dataSize % self.maximumPacketSize) + } + .store(in: &self.cancellables) + } + + private func sendFile() { + self.tryToSendNextPacket() + } + + private func tryToSendNextPacket() { + self.progression.send(self._progression) + if self._progression < 1.0 { + self.sendNextPacket() + } else { + self.process(event: .fileSent) + } + } + + private func sendNextPacket() { + let startIndex = self.currentPacket * self.maximumPacketSize + let endIndex = + self.currentPacket < self.expectedCompletePackets + ? startIndex + self.maximumPacketSize - 1 : startIndex + self.expectedRemainingBytes - 1 + + let dataToSend = globalFirmwareManager.data[startIndex...endIndex] + + if let characteristic { + Robot.shared.connectedPeripheral?.send(dataToSend, forCharacteristic: characteristic) + } + } +} + +// MARK: - StateSendingAssets + +private class StateSendingAssets: GKState, StateEventProcessor { + // MARK: Lifecycle + + override init() { + + self.expectedCompletePackets = 0 + self.expectedRemainingBytes = 0 + + super.init() + } + + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateApplyingUpdate.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + self.loadNextAsset() + } + + override func willExit(to _: GKState) { + self.cancellables.removeAll() + self.characteristic = nil + } + + func process(event: UpdateEvent) { + switch event { + case .assetsSent: + stateMachine?.enter(StateApplyingUpdate.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // NEW + + var assets: [String] = [ +// "1-ACTIVITE REUSSIE", +// "3-BATTERIE FAIBLE", +// "8-FIN DE CHARGE", +// "21-TAG DETEC", +// "25-CONNEXION BT", +// "27-BATTERIE MINIME", +// "37-WELCOME", + ] + var assetsIndex = 0 + var data = Data() + + func loadNextAsset() { + if assetsIndex == assets.count { + self.process(event: .assetsSent) + return + } + + debugPrint("fileURL \(assets[assetsIndex])") + guard let fileURL = Bundle.main.url(forResource: assets[assetsIndex], withExtension: "wav") else { + return + } + + debugPrint("Data \(assets[assetsIndex])") + do { + self.data = try Data(contentsOf: fileURL) + + let dataSize = self.data.count + + self.expectedCompletePackets = Int(floor(Double(dataSize / self.maximumPacketSize))) + self.expectedRemainingBytes = Int(dataSize % self.maximumPacketSize) + + debugPrint("DataSize: \(dataSize): \(self.expectedCompletePackets) packets (\(maximumPacketSize) bytes size) + \(self.expectedRemainingBytes) bytes") + + self.currentPacket = 0 + + debugPrint("Set destination") + let directory = "/fs/home/wav" + let filename = "\(assets[assetsIndex]).wav" + let destinationPath = directory + "/" + filename + debugPrint("Destination path: \(destinationPath)") + + let characteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.filePath, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + debugPrint("Clear path") + let data = Data([1]) + + let characteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.clearFile, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + DispatchQueue.main.asyncAfter(deadline: .now() + 10) { + debugPrint("Send file") + self.sendFile() + } + } + ) + + Robot.shared.connectedPeripheral?.send(data, forCharacteristic: characteristic) + } + } + ) + + Robot.shared.connectedPeripheral?.send(destinationPath.data(using: .utf8)!, forCharacteristic: characteristic) + + self.assetsIndex = assetsIndex + 1 + + return + } catch { + debugPrint("An error occurs (UpdateProcessV150") + return + } + } + + // MARK: Private + + private var cancellables: Set = [] + + private let maximumPacketSize: Int = 182 // MTU(max) - 3 + + private var currentPacket: Int = 0 + private var expectedCompletePackets: Int + private var expectedRemainingBytes: Int + private lazy var characteristic: CharacteristicModelWriteOnly? = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.fileReceptionBuffer, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + self.currentPacket += 1 + self.tryToSendNextPacket() + } + ) + + private var expectedPackets: Int { + self.expectedRemainingBytes == 0 ? self.expectedCompletePackets : self.expectedCompletePackets + 1 + } + + private var _progression: Float { + Float(self.currentPacket) / Float(self.expectedPackets) + } + + private func sendFile() { + self.tryToSendNextPacket() + } + + private func tryToSendNextPacket() { + if self._progression < 1.0 { + self.sendNextPacket() + } else { + self.loadNextAsset() + // self.process(event: .fileSent) + } + } + + private func sendNextPacket() { + let startIndex = self.currentPacket * self.maximumPacketSize + let endIndex = + self.currentPacket < self.expectedCompletePackets + ? startIndex + self.maximumPacketSize - 1 : startIndex + self.expectedRemainingBytes - 1 + + let dataToSend = self.data[startIndex...endIndex] + + if let characteristic { + Robot.shared.connectedPeripheral?.send(dataToSend, forCharacteristic: characteristic) + } + } +} + +// MARK: - StateApplyingUpdate + +private class StateApplyingUpdate: GKState, StateEventProcessor { + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateWaitingForRobotToReboot.Type + } + + override func didEnter(from _: GKState?) { + self.setMajor() + } + + override func willExit(to _: GKState) { + self.cancellables.removeAll() + } + + func process(event: UpdateEvent) { + switch event { + case .robotDisconnected: + stateMachine?.enter(StateWaitingForRobotToReboot.self) + default: + return + } + } + + // MARK: Private + + private var cancellables: Set = [] + + private func setMajor() { + let majorData = Data([globalFirmwareManager.major]) + + let majorCharacteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FirmwareUpdate.Characteristics.versionMajor, + serviceUUID: BLESpecs.FirmwareUpdate.service, + onWrite: self.setMinor + ) + + Robot.shared.connectedPeripheral?.send(majorData, forCharacteristic: majorCharacteristic) + } + + private func setMinor() { + let minorData = Data([globalFirmwareManager.minor]) + + let minorCharacteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FirmwareUpdate.Characteristics.versionMinor, + serviceUUID: BLESpecs.FirmwareUpdate.service, + onWrite: self.setRevision + ) + + Robot.shared.connectedPeripheral?.send(minorData, forCharacteristic: minorCharacteristic) + } + + private func setRevision() { + let revisionData = globalFirmwareManager.revision.data + + let revisionCharacteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FirmwareUpdate.Characteristics.versionRevision, + serviceUUID: BLESpecs.FirmwareUpdate.service, + onWrite: self.applyUpdate + ) + + Robot.shared.connectedPeripheral?.send(revisionData, forCharacteristic: revisionCharacteristic) + } + + private func applyUpdate() { + let applyValue = Data([1]) + + let characteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FirmwareUpdate.Characteristics.requestUpdate, + serviceUUID: BLESpecs.FirmwareUpdate.service + ) + + Robot.shared.connectedPeripheral?.send(applyValue, forCharacteristic: characteristic) + } +} + +// MARK: - StateWaitingForRobotToReboot + +private class StateWaitingForRobotToReboot: GKState, StateEventProcessor { + // MARK: Lifecycle + + init(expectedRobot: RobotPeripheral?) { + self.expectedRobot = expectedRobot + } + + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateFinal.Type || stateClass is StateErrorRobotNotUpToDate.Type + || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + self.registerScanForRobot() + } + + override func willExit(to _: GKState) { + self.cancellables.removeAll() + } + + func process(event: UpdateEvent) { + switch event { + case .robotDetected: + if self.isRobotUpToDate { + stateMachine?.enter(StateFinal.self) + } else { + stateMachine?.enter(StateErrorRobotNotUpToDate.self) + } + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private var cancellables: Set = [] + + private var expectedRobot: RobotPeripheral? + private var isRobotUpToDate: Bool = false + + private func registerScanForRobot() { + BLEManager.shared.scanForRobots() + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { _ in + // nothing to do + }, + receiveValue: { robotDiscoveryList in + let robotDetected = robotDiscoveryList.first { robotDiscovery in + robotDiscovery.robotPeripheral == self.expectedRobot + } + if let robotDetected { + self.isRobotUpToDate = + Version(robotDetected.osVersion) == globalFirmwareManager.currentVersion + + self.process(event: .robotDetected) + } + } + ) + .store(in: &self.cancellables) + } +} + +// MARK: - StateFinal + +private class StateFinal: GKState {} + +// MARK: - StateError + +private protocol StateError {} + +// MARK: - StateErrorFailedToLoadFile + +private class StateErrorFailedToLoadFile: GKState, StateError {} + +// MARK: - StateErrorRobotNotUpToDate + +private class StateErrorRobotNotUpToDate: GKState, StateError {} + +// MARK: - StateErrorRobotUnexpectedDisconnection + +private class StateErrorRobotUnexpectedDisconnection: GKState, StateError {} + +// MARK: - UpdateProcessV150 + +class UpdateProcessV150: UpdateProcessProtocol { + // MARK: Lifecycle + + init() { + self.stateMachine = GKStateMachine(states: [ + StateInitial(), + + StateLoadingUpdateFile(), + StateSettingFileExchangeState(), + StateSettingDestinationPath(), + StateClearingFile(), + self.stateSendingFile, + StateSendingAssets(), + StateApplyingUpdate(), + StateWaitingForRobotToReboot(expectedRobot: Robot.shared.connectedPeripheral), + + StateFinal(), + + StateErrorFailedToLoadFile(), + StateErrorRobotNotUpToDate(), + StateErrorRobotUnexpectedDisconnection(), + ]) + self.stateMachine?.enter(StateInitial.self) + + self.startRoutineToUpdateCurrentState() + self.registerDidDisconnect() + self.sendingFileProgression = self.stateSendingFile.progression + } + + // MARK: Public + + // MARK: - Public variables + + public var currentStage = CurrentValueSubject(.initial) + public var sendingFileProgression = CurrentValueSubject(0.0) + + public func startProcess() { + self.process(event: .startUpdateRequested) + } + + // MARK: Private + + // MARK: - Private variables + + private var stateMachine: GKStateMachine? + private var stateSendingFile = StateSendingFile() + + private var cancellables: Set = [] + + private func process(event: UpdateEvent) { + guard let state = stateMachine?.currentState as? any StateEventProcessor else { + return + } + + state.process(event: event) + + self.updateCurrentState() + } + + private func registerDidDisconnect() { + BLEManager.shared.didDisconnect + .receive(on: DispatchQueue.main) + .sink { + self.process(event: .robotDisconnected) + } + .store(in: &self.cancellables) + } + + private func startRoutineToUpdateCurrentState() { + Timer.publish(every: 1, on: .main, in: .default) + .autoconnect() + .sink { _ in + self.updateCurrentState() + } + .store(in: &self.cancellables) + } + + private func updateCurrentState() { + guard let state = stateMachine?.currentState else { return } + + switch state { + case is StateInitial: + self.currentStage.send(.initial) + case is StateLoadingUpdateFile, + is StateSettingFileExchangeState, + is StateSettingDestinationPath, + is StateClearingFile, + is StateSendingFile, + is StateSendingAssets: + self.currentStage.send(.sendingUpdate) + case is StateApplyingUpdate, + is StateWaitingForRobotToReboot: + self.currentStage.send(.installingUpdate) + case is StateFinal: + self.currentStage.send(completion: .finished) + case is any StateError: + self.sendError(state: state) + default: + self.currentStage.send(completion: .failure(.unknown)) + } + } + + private func sendError(state: GKState) { + switch state { + case is StateErrorFailedToLoadFile: + self.currentStage.send(completion: .failure(.failedToLoadFile)) + case is StateErrorRobotNotUpToDate: + self.currentStage.send(completion: .failure(.robotNotUpToDate)) + case is StateErrorRobotUnexpectedDisconnection: + self.currentStage.send(completion: .failure(.robotUnexpectedDisconnection)) + default: + self.currentStage.send(completion: .failure(.unknown)) + } + } +} + +// swiftlint:enable file_length diff --git a/Apps/LekaUpdater/Sources/Localization.swift b/Apps/LekaUpdater/Sources/Localization.swift new file mode 100644 index 0000000000..b534f492ac --- /dev/null +++ b/Apps/LekaUpdater/Sources/Localization.swift @@ -0,0 +1,171 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import LocalizationKit + +// swiftlint:disable type_name nesting line_length identifier_name +// swift-format-ignore +extension l10n { + enum general { + static let yes = LocalizedString("general.yes", value: "Yes", comment: "Yes") + static let no = LocalizedString("general.no", value: "No", comment: "No") + } + + enum main { + static let appName = LocalizedString("main.app_name", value: "Leka Updater", comment: "Name of the application") + static let appDescription = LocalizedString( + "main.app_description", value: "The app to update your Leka robots!", + comment: "Description of the application" + ) + } + + enum toolbar { + static let connectionButton = LocalizedString( + "toolbar.connection_button", value: "Connection", comment: "Connection toolbar button" + ) + } + + enum connection { + static let continueButton = LocalizedString( + "connection.continue_button", value: "Continue", comment: "Continue button" + ) + static let searchButton = LocalizedString("connection.search_button", value: "Search", comment: "Search button") + static let connectButton = LocalizedString( + "connection.connect_button", value: "Connect", comment: "Connect button" + ) + static let disconnectButton = LocalizedString( + "connection.disconnect_button", value: "Disconnect", comment: "Disconnect button" + ) + + static let noRobotsFoundText = LocalizedString( + "connection.no_robots_found_text", value: "No robots found...", comment: "No robount found text" + ) + static let searchInviteText = LocalizedString( + "connection.search_invite_text", value: "Press the Search button to find robots around you", + comment: "Search invite text" + ) + + static let robotDiscoveryVersion = LocalizedStringInterpolation( + "connection.robot_discovery_version", value: "LekaOS v%@", comment: "Discovery version LekaOS v..." + ) + } + + enum information { + enum status { + static let robotCannotBeUpdatedText = LocalizedString( + "information.status.robot_cannot_be_updated_text", + value: "⚠️ DEV 🚧\nUpdate process not recognized or not available\n(Error code: #0003)", + comment: "Robot cannot be updated text" + ) + static let robotUpdateAvailable = LocalizedString( + "information.status.robot_update_available", value: "⬆️ New firmware update available 📦", + comment: "Robot firmware update available text" + ) + static let robotIsUpToDate = LocalizedString( + "information.status.robot_is_up_to_date", value: "🤖 Your robot is up-to-date! 🎉 You're all done 👌", + comment: "Robot is up to date text" + ) + } + + enum robot { + static var serialNumber = LocalizedStringInterpolation( + "information.robot.serial_number", value: "Serial Number blablabla: %@", + comment: "Connected robot serial number" + ) + static let battery = LocalizedStringInterpolation( + "information.robot.battery", value: "Battery: %@", comment: "Connected robot battery level" + ) + static let version = LocalizedStringInterpolation( + "information.robot.version", value: "Version: %@", comment: "Connected robot version" + ) + static let isCharging = LocalizedStringInterpolation( + "information.robot.charging_status", value: "Charging: %@", comment: "Connected robot charging status" + ) + } + + static let changelogSectionTitle = LocalizedString( + "information.changelog_section_title", value: "Changelog", comment: "Changelog of latest firmware update" + ) + static let changelogNotFoundText = LocalizedString( + "information.changelog_not_found_text", value: "Changelog is not available", + comment: "Changelog not found text" + ) + + static let startUpdateButton = LocalizedString("information.start_update_button", value: "Start robot update", comment: "Start update button") + } + + enum update { + enum requirements { + static let instructionsText = LocalizedString( + "update.requirements.instructions_text", value: "To start the update, make sure that:", + comment: "Requirements before update" + ) + static let chargingBasePluggedText = LocalizedString( + "update.requirements.charging_base_plugged_text", + value: "The robot is placed on its base and the base is connected to the mains", + comment: "Base must be plugged" + ) + static let chargingBaseGreenLEDText = LocalizedString( + "update.requirements.charging_base_green_led_text", + value: "The charging LED is green indicating the correct positioning on the base", + comment: "Base green LED must be on" + ) + static let robotBatteryMinimumLevelText = LocalizedString( + "update.requirements.robot_battery_minimum_level_text", + value: "The robot battery level is at least 30%", comment: "Robot battery level must be at least 30%" + ) + } + + enum error { + static let failedToLoadFileDescription = LocalizedString("update.error.failed_to_load_file", value: "Robot update file cannot be opened\n(Error code #0001)", comment: "Failed to load file") + static let failedToLoadFileInstructions = LocalizedString("update.error.failed_to_load_file_instructions", value: "Please reinstall the app", comment: "Failed to load file instructions") + + static let robotNotUpToDateDescription = LocalizedString("update.error.robot_not_up_to_date", value: "Update failed\n(Error code #0002)", comment: "Robot not up to date") + static let robotNotUpToDateInstructions = LocalizedString("update.error.robot_not_up_to_date_instructions", value: "Reconnect the robot and restart the process", comment: "Robot not up to date instructions") + + static let updateProcessNotAvailableDescription = LocalizedString("update.error.update_process_not_available", value: "Update process not recognized or not available\n(Error code #0003)", comment: "Update process not available") + static let updateProcessNotAvailableInstructions = LocalizedString("update.error.update_process_not_available_instructions", value: "Please contact technical support", comment: "Update process not available instructions") + + static let robotUnexpectedDisconnectionDescription = LocalizedString("update.error.robot_unexpected_disconnection", value: "The robot has been disconnected\n(Error code #0004)", comment: "Robot unexpected disconnection") + static let robotUnexpectedDisconnectionInstructions = LocalizedString("update.error.robot_unexpected_disconnection_instructions", value: "Restart the robot using the \"Emergency stop\" card,\nreconnect the robot and restart the process", comment: "Robot unexpected disconnection instructions") + + static let unknownErrorDescription = LocalizedString("update.error.unknown_error", value: "An unknown error has occurred\n(Error code #0000)", comment: "unknown error") + static let unknownErrorInstructions = LocalizedString("update.error.unknown_error_instructions", value: "Please contact technical support", comment: "unknown error instructions") + } + + enum alert { + static let robotNotInChargeTitle = LocalizedString("information.status.alert.robot_not_in_charge", value: "⚠️ WARNING ⚡\nThe robot is no longer in charge", comment: "Robot not in charge alert") + + static let robotNotInChargeMessage = LocalizedString("information.status.alert.robot_not_in_charge_button", value: "Please put Leka back on its charging station and/or check it's plugged to a power outlet", comment: "Robot not in charge alert button") + } + + enum sending { + static let sendingTitle = LocalizedString("update.sending.title", value: "Sending update to the robot", comment: "Sending title") + static let instructions = LocalizedString("update.sending.instruction", value: "Do not unplug your robot\nDo not remove it from its charging station\nDo not close the app", comment: "Sending instruction") + } + + enum rebooting { + static let rebootingTitle = LocalizedString("update.rebooting.title", value: "The update is being installed", comment: "Rebooting title") + static let rebootingSubtitle = LocalizedString("update.rebooting.subtitle", value: "Your robot will reboot in a few minutes", comment: "Rebooting subtitle") + } + + enum finished { + static let updateAnotherRobotButton = LocalizedString("update.finished.update_another_robot_button", value: "Update another robot", comment: "Update another robot button") + + static let launchLekaAppButton = LocalizedString("update.finished.launch_leka_app_button", value: "Launch My Leka Alpha 🚀", comment: "Launch Leka app button") + + static let robotUpdatedSuccessfully = LocalizedString("update.finished.robot_updated_successfully", value: "Congrats! 🥳\nYour robot is now up-to-date 🎉", comment: "Robot updated successfully") + } + + static let stepNumber = LocalizedStringInterpolation("update.step_number", value: "Step %@", comment: "Update step number") + + static let errorTitle = LocalizedString("update.error", value: "An error has occurred", comment: "Update error") + static let errorDescription = LocalizedString("update.error_description", value: "Unknown error", comment: "Update error description") + static let errorCallToAction = LocalizedString("update.error_call_to_action", value: "Contact technical support", comment: "Update error call to action") + static let errorBackButtonTitle = LocalizedString("update.error_back_button", value: "Return to robot connection page", comment: "Update error back button") + } +} + +// swiftlint:enable type_name nesting line_length identifier_name diff --git a/Apps/LekaUpdater/Sources/MainApp.swift b/Apps/LekaUpdater/Sources/MainApp.swift new file mode 100644 index 0000000000..c9a0b783a2 --- /dev/null +++ b/Apps/LekaUpdater/Sources/MainApp.swift @@ -0,0 +1,20 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import DesignKit +import SwiftUI + +var globalFirmwareManager = FirmwareManager() + +// MARK: - LekaUpdaterApp + +@main +struct LekaUpdaterApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/Apps/LekaUpdater/Sources/View/ConnectionView/ConnectionView.swift b/Apps/LekaUpdater/Sources/View/ConnectionView/ConnectionView.swift new file mode 100644 index 0000000000..48d130979f --- /dev/null +++ b/Apps/LekaUpdater/Sources/View/ConnectionView/ConnectionView.swift @@ -0,0 +1,30 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit +import RobotKit +import SwiftUI + +struct ConnectionView: View { + var body: some View { + RobotConnectionView() + .toolbar { + ToolbarItem(placement: .principal) { + VStack { + Text(l10n.main.appName) + .font(.title2) + .bold() + Text(l10n.main.appDescription) + } + .foregroundColor(.lkNavigationTitle) + } + } + } +} + +#Preview { + NavigationStack { + ConnectionView() + } +} diff --git a/Apps/LekaUpdater/Sources/View/ContentView.swift b/Apps/LekaUpdater/Sources/View/ContentView.swift new file mode 100644 index 0000000000..99c3761a31 --- /dev/null +++ b/Apps/LekaUpdater/Sources/View/ContentView.swift @@ -0,0 +1,35 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// MARK: - ContentView + +struct ContentView: View { + @State var isConnectionViewPresented = true + @State var isUpdateStatusViewPresented = false + + var body: some View { + NavigationStack { + InformationView( + isConnectionViewPresented: self.$isConnectionViewPresented, + isUpdateStatusViewPresented: self.$isUpdateStatusViewPresented + ) + .fullScreenCover(isPresented: self.$isConnectionViewPresented) { + NavigationStack { + ConnectionView() + } + } + .fullScreenCover(isPresented: self.$isUpdateStatusViewPresented) { + NavigationStack { + UpdateStatusView(isConnectionViewPresented: self.$isConnectionViewPresented) + } + } + } + } +} + +#Preview { + ContentView() +} diff --git a/Apps/LekaUpdater/Sources/View/InformationView/ChangelogView.swift b/Apps/LekaUpdater/Sources/View/InformationView/ChangelogView.swift new file mode 100644 index 0000000000..6252429161 --- /dev/null +++ b/Apps/LekaUpdater/Sources/View/InformationView/ChangelogView.swift @@ -0,0 +1,49 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - ChangelogView + +struct ChangelogView: View { + // MARK: Internal + + var body: some View { + Text(self.changelog) + } + + // MARK: Private + + private var changelog: LocalizedStringKey { + // swiftlint:disable:next force_cast + let osVersion = Bundle.main.object(forInfoDictionaryKey: "LEKA_OS_VERSION") as! String + var languageCode: String { + guard let language = Locale.current.language.languageCode?.identifier else { return "en" } + return language == "fr" ? "fr" : "en" + } + + let fileURL = Bundle.main.url( + forResource: "LekaOS-\(osVersion)-\(languageCode)", + withExtension: "md" + )! + + do { + let content = try String(contentsOf: fileURL) + return LocalizedStringKey(stringLiteral: content) + } catch { + return "\(l10n.information.changelogNotFoundText)" + } + } +} + +// MARK: - ChangelogView_Previews + +struct ChangelogView_Previews: PreviewProvider { + static var previews: some View { + ChangelogView() + .foregroundColor(DesignKitAsset.Colors.darkGray.swiftUIColor) + } +} diff --git a/Apps/LekaUpdater/Sources/View/InformationView/InformationView.swift b/Apps/LekaUpdater/Sources/View/InformationView/InformationView.swift new file mode 100644 index 0000000000..6d038cae0f --- /dev/null +++ b/Apps/LekaUpdater/Sources/View/InformationView/InformationView.swift @@ -0,0 +1,146 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import RobotKit +import SwiftUI +import Version + +// MARK: - InformationView + +struct InformationView: View { + // MARK: Internal + + @StateObject var viewModel = InformationViewModel() + @Binding var isConnectionViewPresented: Bool + @Binding var isUpdateStatusViewPresented: Bool + + var body: some View { + VStack { + ScrollView { + VStack(alignment: .center, spacing: 10) { + if self.viewModel.showRobotCannotBeUpdated { + RobotCannotBeUpdatedIllustration(size: 200) + + Text(self.viewModel.robotName) + .font(.title3) + + Text( + l10n.information.status.robotCannotBeUpdatedText.characters + + " - (LekaOS v\(self.viewModel.robotOSVersion))" + ) + .font(.title2) + .multilineTextAlignment(.center) + + } else if self.viewModel.showRobotNeedsUpdate { + RobotNeedsUpdateIllustration(size: 200) + + Text(self.viewModel.robotName) + .font(.title3) + + Text(l10n.information.status.robotUpdateAvailable) + .font(.title2) + } else { + RobotUpToDateIllustration(size: 200) + + Text(self.viewModel.robotName) + .font(.title3) + + Text(l10n.information.status.robotIsUpToDate) + .font(.title2) + } + } + .padding([.bottom], 10) + + RobotInformationView() + .padding() + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(.lkStroke, lineWidth: 3) + ) + .padding([.horizontal], 3) + .padding([.vertical], 10) + + DisclosureGroup { + ChangelogView() + .padding() + } label: { + Text(l10n.information.changelogSectionTitle) + .foregroundStyle(DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor) + } + .accentColor(DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor) + .padding() + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(.lkStroke, lineWidth: 3) + ) + .padding([.horizontal], 3) + .padding([.vertical], 10) + + if self.viewModel.showRobotNeedsUpdate { + RobotUpdateAvailableView(isUpdateStatusViewPresented: self.$isUpdateStatusViewPresented) + .padding([.vertical], 10) + } + + VStack { + LekaUpdaterAsset.Assets.lekaUpdaterIcon.swiftUIImage + .resizable() + .scaledToFit() + .frame(height: 70) + .padding(35) + } + } + .padding([.horizontal], 20) + } + .foregroundColor(DesignKitAsset.Colors.darkGray.swiftUIColor) + .background(.lkBackground) + .onChange(of: self.isViewVisible) { isVisible in + if isVisible { self.viewModel.onViewReappear() } + } + .toolbar { + ToolbarItem(placement: .principal) { + VStack { + Text(l10n.main.appName) + .font(.title2) + .bold() + Text(l10n.main.appDescription) + } + .foregroundColor(.lkNavigationTitle) + } + + ToolbarItem(placement: .navigationBarLeading) { + Button { + self.isConnectionViewPresented = true + } label: { + HStack { + Image(systemName: "chevron.backward") + Text(l10n.toolbar.connectionButton) + } + } + } + } + } + + // MARK: Private + + private var isViewVisible: Bool { + !self.isConnectionViewPresented && !self.isUpdateStatusViewPresented + } +} + +#Preview { + NavigationStack { + InformationView( + isConnectionViewPresented: .constant(false), + isUpdateStatusViewPresented: .constant(false) + ) + .onAppear { + Robot.shared.name.send("Leka") + Robot.shared.battery.send(75) + Robot.shared.isCharging.send(true) + Robot.shared.osVersion.send(Version(1, 3, 0)) + } + } +} diff --git a/Apps/LekaUpdater/Sources/View/InformationView/RequirementsView.swift b/Apps/LekaUpdater/Sources/View/InformationView/RequirementsView.swift new file mode 100644 index 0000000000..68a07c266b --- /dev/null +++ b/Apps/LekaUpdater/Sources/View/InformationView/RequirementsView.swift @@ -0,0 +1,85 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - RequirementsView + +struct RequirementsView: View { + @StateObject var viewModel: RequirementsViewModel + + var body: some View { + VStack { + Text(self.viewModel.requirementsInstructionsText) + .foregroundColor(DesignKitAsset.Colors.darkGray.swiftUIColor) + + HStack(alignment: .top) { + RequirementView( + image: self.viewModel.chargingBasePluggedImage, + text: self.viewModel.chargingBasePluggedText, + stepNumber: 1 + ) + + RequirementView( + image: self.viewModel.chargingBaseGreenLEDImage, + text: self.viewModel.chargingBaseGreenLEDText, + stepNumber: 2 + ) + + RequirementView( + image: self.viewModel.robotBatteryMinimumLevelImage, + text: self.viewModel.robotBatteryMinimumLevelText, + stepNumber: 3 + ) + } + } + .padding() + } +} + +// MARK: - RequirementView + +private struct RequirementView: View { + let image: Image + let text: AttributedString + let stepNumber: Int + + var body: some View { + VStack { + self.image + .resizable() + .scaledToFit() + .frame(height: 150) + .clipShape(Circle()) + .overlay( + Circle() + .strokeBorder(DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor, lineWidth: 2) + ) + + Text("\(self.stepNumber)") + .font(.title2) + .foregroundColor(DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor) + .padding() + + Text(self.text) + .font(.caption) + .multilineTextAlignment(.center) + .foregroundColor(DesignKitAsset.Colors.darkGray.swiftUIColor) + } + .padding() + } +} + +// MARK: - RequirementsView_Previews + +struct RequirementsView_Previews: PreviewProvider { + static var previews: some View { + RequirementsView(viewModel: RequirementsViewModel()) + .environment(\.locale, .init(identifier: "en")) + + RequirementsView(viewModel: RequirementsViewModel()) + .environment(\.locale, .init(identifier: "fr")) + } +} diff --git a/Apps/LekaUpdater/Sources/View/InformationView/RobotCannotBeUpdateIllustration.swift b/Apps/LekaUpdater/Sources/View/InformationView/RobotCannotBeUpdateIllustration.swift new file mode 100644 index 0000000000..a1953d54f4 --- /dev/null +++ b/Apps/LekaUpdater/Sources/View/InformationView/RobotCannotBeUpdateIllustration.swift @@ -0,0 +1,94 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - RobotCannotBeUpdatedIllustration + +struct RobotCannotBeUpdatedIllustration: View { + // MARK: Lifecycle + + init(size: CGFloat = 300) { + self.illustrationSize = size + } + + // MARK: Public + + public var illustrationSize: CGFloat = 300 + + // MARK: Internal + + var body: some View { + ZStack { + Circle() + .fill(.lkBackground) + .frame(width: self.illustrationSize) + + Circle() + .strokeBorder( + .gray, + style: StrokeStyle(lineWidth: self.circleLineWidth, lineCap: .round, dash: [self.dashSpacer, self.dashSpacer]) + ) + .frame(width: self.circleSize) + + LekaUpdaterAsset.Assets.robotOnBase.swiftUIImage + .resizable() + .scaledToFit() + .frame(height: self.imageSize) + + VStack { + Spacer() + + ZStack { + Circle().fill(.lkBackground) + .frame(height: self.checkmarkSize) + + Image(systemName: "xmark.circle") + .font(.system(size: self.checkmarkSize)) + .foregroundColor(.gray) + } + } + } + .frame(width: self.circleSize, height: self.illustrationSize) + } + + // MARK: Private + + private var circleSize: CGFloat { + self.illustrationSize * 250 / 300 + } + + private var circleLineWidth: CGFloat { + self.illustrationSize / 60 + } + + private var dashSpacer: CGFloat { + self.illustrationSize / 18 + } + + private var imageSize: CGFloat { + self.illustrationSize * 180 / 300 + } + + private var checkmarkSize: CGFloat { + self.illustrationSize * 56 / 300 + } +} + +// MARK: - RobotCannotBeUpdatedIllustration_Previews + +struct RobotCannotBeUpdatedIllustration_Previews: PreviewProvider { + static var previews: some View { + Form { + Section { + Group { + RobotCannotBeUpdatedIllustration(size: 600) + } + .frame(maxWidth: .infinity, alignment: .center) + } + .listRowBackground(Color.clear) + } + } +} diff --git a/Apps/LekaUpdater/Sources/View/InformationView/RobotInformationView.swift b/Apps/LekaUpdater/Sources/View/InformationView/RobotInformationView.swift new file mode 100644 index 0000000000..a1c7266dd3 --- /dev/null +++ b/Apps/LekaUpdater/Sources/View/InformationView/RobotInformationView.swift @@ -0,0 +1,68 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import RobotKit +import SwiftUI +import Version + +// MARK: - RobotInformationView + +struct RobotInformationView: View { + // MARK: Internal + + var body: some View { + VStack(alignment: .leading) { + Text(l10n.information.robot.serialNumber(self.viewModel.robotSerialNumber)) + Divider() + Text(l10n.information.robot.battery(self.viewModel.robotBattery)) + Divider() + Text(l10n.information.robot.version(self.viewModel.robotOsVersion)) + Divider() + Text(l10n.information.robot.isCharging(self.viewModel.robotIsCharging)) + } + .padding() + } + + // MARK: Private + + @StateObject private var viewModel = RobotInformationViewModel() +} + +// MARK: - RobotInformationView_Previews + +struct RobotInformationView_Previews: PreviewProvider { + static var previews: some View { + VStack { + RobotInformationView() + .foregroundColor(DesignKitAsset.Colors.darkGray.swiftUIColor) + + Button("Change example") { + let selection = Int.random(in: 1...3) + switch selection { + case 1: + print("Robot just connected (not received serial number yet)") + Robot.shared.serialNumber.send("(n/a)") + Robot.shared.battery.send(Int.random(in: 0...100)) + Robot.shared.osVersion.send(Version(1, 0, 0)) + Robot.shared.isCharging.send(false) + case 2: + print("Robot connected") + Robot.shared.serialNumber.send("LK-2206DHQFLQJZ139813JJQ - connected") + Robot.shared.battery.send(Int.random(in: 0...100)) + Robot.shared.osVersion.send(Version(1, 0, 0)) + Robot.shared.isCharging.send(true) + default: + print("Robot not connected") + Robot.shared.serialNumber.send("(n/a)") + Robot.shared.battery.send(0) + Robot.shared.osVersion.send(nil) + Robot.shared.isCharging.send(false) + } + } + } + .environment(\.locale, .init(identifier: "fr")) + } +} diff --git a/Apps/LekaUpdater/Sources/View/InformationView/RobotNeedsUpdateIllustration.swift b/Apps/LekaUpdater/Sources/View/InformationView/RobotNeedsUpdateIllustration.swift new file mode 100644 index 0000000000..7566077ea3 --- /dev/null +++ b/Apps/LekaUpdater/Sources/View/InformationView/RobotNeedsUpdateIllustration.swift @@ -0,0 +1,94 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - RobotNeedsUpdateIllustration + +struct RobotNeedsUpdateIllustration: View { + // MARK: Lifecycle + + init(size: CGFloat = 300) { + self.illustrationSize = size + } + + // MARK: Public + + public var illustrationSize: CGFloat = 300 + + // MARK: Internal + + var body: some View { + ZStack { + Circle() + .fill(.lkBackground) + .frame(width: self.illustrationSize) + + Circle() + .strokeBorder( + .yellow, + style: StrokeStyle(lineWidth: self.circleLineWidth, lineCap: .round, dash: [self.dashSpacer, self.dashSpacer]) + ) + .frame(width: self.circleSize) + + LekaUpdaterAsset.Assets.robotOnBase.swiftUIImage + .resizable() + .scaledToFit() + .frame(height: self.imageSize) + + VStack { + Spacer() + + ZStack { + Circle().fill(.lkBackground) + .frame(height: self.checkmarkSize) + + Image(systemName: "exclamationmark.circle") + .font(.system(size: self.checkmarkSize)) + .foregroundColor(.yellow) + } + } + } + .frame(width: self.circleSize, height: self.illustrationSize) + } + + // MARK: Private + + private var circleSize: CGFloat { + self.illustrationSize * 250 / 300 + } + + private var circleLineWidth: CGFloat { + self.illustrationSize / 60 + } + + private var dashSpacer: CGFloat { + self.illustrationSize / 18 + } + + private var imageSize: CGFloat { + self.illustrationSize * 180 / 300 + } + + private var checkmarkSize: CGFloat { + self.illustrationSize * 56 / 300 + } +} + +// MARK: - RobotNeedsUpdateIllustration_Previews + +struct RobotNeedsUpdateIllustration_Previews: PreviewProvider { + static var previews: some View { + Form { + Section { + Group { + RobotNeedsUpdateIllustration(size: 600) + } + .frame(maxWidth: .infinity, alignment: .center) + } + .listRowBackground(Color.clear) + } + } +} diff --git a/Apps/LekaUpdater/Sources/View/InformationView/RobotUpToDateIllustration.swift b/Apps/LekaUpdater/Sources/View/InformationView/RobotUpToDateIllustration.swift new file mode 100644 index 0000000000..48d848d050 --- /dev/null +++ b/Apps/LekaUpdater/Sources/View/InformationView/RobotUpToDateIllustration.swift @@ -0,0 +1,93 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - RobotUpToDateIllustration + +struct RobotUpToDateIllustration: View { + // MARK: Lifecycle + + init(size: CGFloat = 300) { + self.illustrationSize = size + } + + // MARK: Public + + public var illustrationSize: CGFloat = 300 + + // MARK: Internal + + var body: some View { + ZStack { + Circle() + .fill(.lkBackground) + .frame(width: self.illustrationSize) + + Circle() + .strokeBorder( + DesignKitAsset.Colors.lekaGreen.swiftUIColor, lineWidth: self.circleLineWidth + ) + .frame(width: self.circleSize) + + LekaUpdaterAsset.Assets.robotOnBase.swiftUIImage + .resizable() + .scaledToFit() + .frame(height: self.imageSize) + + VStack { + Spacer() + + ZStack { + Circle().fill(.lkBackground) + .frame(height: self.checkmarkSize) + + Image(systemName: "checkmark.circle") + .font(.system(size: self.checkmarkSize)) + .foregroundColor(DesignKitAsset.Colors.lekaGreen.swiftUIColor) + } + } + } + .frame(width: self.circleSize, height: self.illustrationSize) + } + + // MARK: Private + + private var circleSize: CGFloat { + self.illustrationSize * 250 / 300 + } + + private var circleLineWidth: CGFloat { + self.illustrationSize / 60 + } + + private var dashSpacer: CGFloat { + self.illustrationSize / 18 + } + + private var imageSize: CGFloat { + self.illustrationSize * 180 / 300 + } + + private var checkmarkSize: CGFloat { + self.illustrationSize * 56 / 300 + } +} + +// MARK: - RobotUpToDateIllustration_Previews + +struct RobotUpToDateIllustration_Previews: PreviewProvider { + static var previews: some View { + Form { + Section { + Group { + RobotUpToDateIllustration(size: 600) + } + .frame(maxWidth: .infinity, alignment: .center) + } + .listRowBackground(Color.clear) + } + } +} diff --git a/Apps/LekaUpdater/Sources/View/InformationView/RobotUpdateAvailableView.swift b/Apps/LekaUpdater/Sources/View/InformationView/RobotUpdateAvailableView.swift new file mode 100644 index 0000000000..2012bbea9a --- /dev/null +++ b/Apps/LekaUpdater/Sources/View/InformationView/RobotUpdateAvailableView.swift @@ -0,0 +1,75 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import RobotKit +import SwiftUI + +// MARK: - RobotUpdateAvailableView + +struct RobotUpdateAvailableView: View { + @StateObject private var requirementsViewModel = RequirementsViewModel() + + @Binding var isUpdateStatusViewPresented: Bool + + var body: some View { + VStack { + Text(l10n.information.status.robotUpdateAvailable) + .font(.title3) + .padding([.bottom]) + + Button { + self.isUpdateStatusViewPresented = true + } label: { + Text(l10n.information.startUpdateButton) + .foregroundColor(.white) + .font(.title2) + .padding([.horizontal], 50) + .padding([.vertical], 30) + .background(DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor) + .cornerRadius(10) + .frame(maxWidth: .infinity) + } + .buttonStyle(.plain) + .disabled(self.requirementsViewModel.robotIsNotReadyToUpdate) + + if !self.requirementsViewModel.robotIsReadyToUpdate { + RequirementsView(viewModel: self.requirementsViewModel) + } + } + .padding() + } +} + +// MARK: - RobotUpdateAvailableView_Previews + +struct RobotUpdateAvailableView_Previews: PreviewProvider { + @State static var isUpdateStatusViewPresented = false + + static var previews: some View { + VStack { + Spacer() + + RobotUpdateAvailableView(isUpdateStatusViewPresented: $isUpdateStatusViewPresented) + .foregroundColor(DesignKitAsset.Colors.darkGray.swiftUIColor) + + Spacer() + + Button("Change example") { + let isReady = Bool.random() + + if isReady { + print("Robot is ready to update") + Robot.shared.battery.send(100) + Robot.shared.isCharging.send(true) + } else { + print("Robot is NOT ready to update") + Robot.shared.battery.send(0) + Robot.shared.isCharging.send(false) + } + } + } + } +} diff --git a/Apps/LekaUpdater/Sources/View/UpdateStatusDemoView.swift b/Apps/LekaUpdater/Sources/View/UpdateStatusDemoView.swift new file mode 100644 index 0000000000..0d59d8dd85 --- /dev/null +++ b/Apps/LekaUpdater/Sources/View/UpdateStatusDemoView.swift @@ -0,0 +1,115 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import SwiftUI + +// MARK: - UpdateStatusDemoViewModel + +class UpdateStatusDemoViewModel: ObservableObject { + // MARK: Lifecycle + + init() { + self.updateProcessController = UpdateProcessController() + + self.subscribeToStateUpdates() + } + + // MARK: Public + + @Published public var state = "" + @Published public var error: String = "" + + public func startUpdate() { + self.updateProcessController.startUpdate() + } + + // MARK: Private + + private var updateProcessController: UpdateProcessController + + private var cancellables: Set = [] + + private func subscribeToStateUpdates() { + self.updateProcessController.currentStage + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .finished: + self.state = "Votre robot est maintenant à jour!" + case let .failure(error): + self.state = "Oops, something wrong happened" + + switch error { + case .updateProcessNotAvailable: + self.error = "ERROR, this robot cannot be update" + case .failedToLoadFile: + self.error = "ERROR, please reinstall the app" + case .robotNotUpToDate: + self.error = "ERROR, please try again" + default: + self.error = "ERROR, unknown" + } + } + } receiveValue: { state in + switch state { + case .initial: + self.state = "initialization" + case .sendingUpdate: + self.state = "sending update" + case .installingUpdate: + self.state = "installing update" + } + } + .store(in: &self.cancellables) + } +} + +// MARK: - UpdateStatusDemoView + +struct UpdateStatusDemoView: View { + // MARK: Internal + + var body: some View { + VStack { + Text(verbatim: "Update Status Demo") + .font(.largeTitle) + .padding(.top) + + Divider() + Spacer() + + VStack { + VStack { + Text(verbatim: "User state is: \(self.viewModel.state)") + .font(.title2) + .bold() + + Button(action: self.viewModel.startUpdate) { + Text(verbatim: "Start Update") + } + + Text(verbatim: "Error: \(self.viewModel.error)") + .foregroundColor(.red) + .opacity(self.viewModel.error.isEmpty ? 0.0 : 1.0) + } + .padding() + } + + Spacer() + } + } + + // MARK: Private + + @StateObject private var viewModel = UpdateStatusDemoViewModel() +} + +// MARK: - UpdateStatusDemoView_Previews + +struct UpdateStatusDemoView_Previews: PreviewProvider { + static var previews: some View { + UpdateStatusDemoView() + } +} diff --git a/Apps/LekaUpdater/Sources/View/UpdatingViews/ErrorView.swift b/Apps/LekaUpdater/Sources/View/UpdatingViews/ErrorView.swift new file mode 100644 index 0000000000..7a403af171 --- /dev/null +++ b/Apps/LekaUpdater/Sources/View/UpdatingViews/ErrorView.swift @@ -0,0 +1,86 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - ErrorIllustration + +struct ErrorIllustration: View { + var body: some View { + Image(systemName: "exclamationmark.octagon.fill") + .resizable() + .scaledToFit() + .foregroundColor(.red) + } +} + +// MARK: - ErrorContentView + +struct ErrorContentView: View { + @Environment(\.dismiss) var dismiss + + public let errorDescription: String + public let errorInstruction: String + + @Binding var isConnectionViewPresented: Bool + + var body: some View { + VStack(spacing: 15) { + Text(self.errorDescription) + .font(.title2) + .bold() + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.center) + + Text(self.errorInstruction) + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.center) + + Button { + self.dismiss() + self.isConnectionViewPresented = true + } label: { + Text(l10n.update.errorBackButtonTitle) + .padding(.horizontal) + .foregroundColor(.white) + .frame(height: 50) + .background(DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor) + .cornerRadius(10) + } + .buttonStyle(.plain) + .padding(.top) + .shadow(radius: 3, y: 4) + } + } +} + +// MARK: - ErrorView_Previews + +struct ErrorView_Previews: PreviewProvider { + @State static var isConnectionViewPresented = false + + static let errorDescription = l10n.update.errorDescription + static let errorActionRequired = l10n.update.errorCallToAction + + static var previews: some View { + VStack(spacing: 40) { + ErrorIllustration() + .frame(height: 250) + + Text(l10n.update.errorTitle) + .font(.title) + .bold() + .monospacedDigit() + + ErrorContentView( + errorDescription: String(errorDescription.characters), + errorInstruction: String(errorActionRequired.characters), + isConnectionViewPresented: $isConnectionViewPresented + ) + .foregroundColor(DesignKitAsset.Colors.darkGray.swiftUIColor) + } + } +} diff --git a/Apps/LekaUpdater/Sources/View/UpdatingViews/RebootingView.swift b/Apps/LekaUpdater/Sources/View/UpdatingViews/RebootingView.swift new file mode 100644 index 0000000000..36b969379b --- /dev/null +++ b/Apps/LekaUpdater/Sources/View/UpdatingViews/RebootingView.swift @@ -0,0 +1,62 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - RebootingIllustration + +struct RebootingIllustration: View { + var body: some View { + LekaUpdaterAsset.Assets.updateInstallation.swiftUIImage + .resizable() + .scaledToFit() + } +} + +// MARK: - RebootingContentView + +struct RebootingContentView: View { + var body: some View { + VStack { + Text(l10n.update.rebooting.rebootingTitle) + .font(.title2) + .bold() + + ProgressView() + .scaleEffect(2) + .padding() + .padding() + + Text(l10n.update.rebooting.rebootingSubtitle) + } + } +} + +// MARK: - RebootingView_Previews + +struct RebootingView_Previews: PreviewProvider { + static var previews: some View { + VStack { + RebootingIllustration() + .frame(height: 250) + .padding(.bottom) + .padding(.bottom) + + Text(l10n.update.stepNumber("2/3")) + .font(.title) + .bold() + .monospacedDigit() + .padding() + + VStack { + RebootingContentView() + Spacer() + } + .foregroundColor(DesignKitAsset.Colors.darkGray.swiftUIColor) + .frame(maxWidth: .infinity, maxHeight: 250) + } + } +} diff --git a/Apps/LekaUpdater/Sources/View/UpdatingViews/SendingFileView.swift b/Apps/LekaUpdater/Sources/View/UpdatingViews/SendingFileView.swift new file mode 100644 index 0000000000..35f85d584c --- /dev/null +++ b/Apps/LekaUpdater/Sources/View/UpdatingViews/SendingFileView.swift @@ -0,0 +1,77 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - SendingFileIllustration + +struct SendingFileIllustration: View { + var body: some View { + LekaUpdaterAsset.Assets.sendingUpdate.swiftUIImage + .resizable() + .scaledToFit() + } +} + +// MARK: - SendingFileContentView + +struct SendingFileContentView: View { + @Binding var progress: Float + + var body: some View { + VStack { + Text(l10n.update.sending.sendingTitle) + .font(.title2) + .bold() + + Gauge(value: self.progress, label: { EmptyView() }) + .tint(Color(red: 160 / 255, green: 185 / 255, blue: 49 / 255)) + .padding(40) + .background( + RoundedRectangle(cornerRadius: 25) + .fill(.white) + .shadow(radius: 3, y: 4) + ) + .frame(width: 600) + + Text(l10n.update.sending.instructions) + .multilineTextAlignment(.center) + .padding() + } + } +} + +// MARK: - SendingFileView_Previews + +struct SendingFileView_Previews: PreviewProvider { + // MARK: Internal + + static var previews: some View { + VStack { + SendingFileIllustration() + .frame(height: 250) + .padding(.bottom) + .padding(.bottom) + + Text(l10n.update.stepNumber("1/3")) + .font(.title) + .bold() + .monospacedDigit() + .padding() + + VStack { + SendingFileContentView(progress: $progress) + Spacer() + } + .foregroundColor(DesignKitAsset.Colors.darkGray.swiftUIColor) + .frame(maxWidth: .infinity, maxHeight: 250) + } + } + + // MARK: Private + + @State private static var progress: Float = 0.66 +} diff --git a/Apps/LekaUpdater/Sources/View/UpdatingViews/UpdateFinishedView.swift b/Apps/LekaUpdater/Sources/View/UpdatingViews/UpdateFinishedView.swift new file mode 100644 index 0000000000..2993e91400 --- /dev/null +++ b/Apps/LekaUpdater/Sources/View/UpdatingViews/UpdateFinishedView.swift @@ -0,0 +1,127 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - UpdateFinishedIllustration + +struct UpdateFinishedIllustration: View { + var body: some View { + ZStack { + Circle().fill(.lkBackground) + + Circle().strokeBorder(DesignKitAsset.Colors.lekaGreen.swiftUIColor, lineWidth: 5) + + LekaUpdaterAsset.Assets.robotOnBase.swiftUIImage + .resizable() + .scaledToFit() + .frame(height: 180) + + VStack { + Spacer() + + ZStack { + Circle().fill(.lkBackground) + .frame(height: 56) + + Image(systemName: "checkmark.circle") + .font(.system(size: 56)) + .foregroundColor(DesignKitAsset.Colors.lekaGreen.swiftUIColor) + } + } + } + .frame(width: 250, height: 300) + .padding() + } +} + +// MARK: - UpdateFinishedContentView + +struct UpdateFinishedContentView: View { + @Environment(\.dismiss) var dismiss + + @Binding var isConnectionViewPresented: Bool + + var body: some View { + VStack { + Text(l10n.update.finished.robotUpdatedSuccessfully) + .multilineTextAlignment(.center) + .font(.title2) + .bold() + + VStack(spacing: 20) { + Button { + self.dismiss() + self.isConnectionViewPresented = true + } label: { + Text(l10n.update.finished.updateAnotherRobotButton) + .foregroundColor(DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor) + .frame(maxWidth: .infinity, maxHeight: 50) + .background( + ZStack { + RoundedRectangle(cornerRadius: 10).fill(.white) + RoundedRectangle(cornerRadius: 10) + .strokeBorder( + DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor, lineWidth: 2 + ) + } + ) + .cornerRadius(10) + } + + Button { + let appURL = URL( + string: "LekaApp://") + let appStoreURL = URL(string: "https://apps.apple.com/app/mon-leka-alpha/id1607862221")! + + if let appURL, UIApplication.shared.canOpenURL(appURL) { + UIApplication.shared.open(appURL, options: [:], completionHandler: nil) + } else { + UIApplication.shared.open(appStoreURL, options: [:], completionHandler: nil) + } + } label: { + Text(l10n.update.finished.launchLekaAppButton) + .foregroundColor(.white) + .frame(maxWidth: .infinity, maxHeight: 50) + .background(DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor) + .cornerRadius(10) + } + .buttonStyle(.plain) + } + .shadow(radius: 3, y: 4) + .padding(.top) + .padding(.horizontal, 200) + } + } +} + +// MARK: - UpdateFinishedView_Previews + +struct UpdateFinishedView_Previews: PreviewProvider { + @State static var isConnectionViewPresented = false + + static var previews: some View { + VStack { + UpdateFinishedIllustration() + .frame(height: 250) + .padding(.bottom) + .padding(.bottom) + + Text(l10n.update.stepNumber("3/3")) + .font(.title) + .bold() + .monospacedDigit() + .padding() + + VStack { + UpdateFinishedContentView(isConnectionViewPresented: $isConnectionViewPresented) + Spacer() + } + .foregroundColor(DesignKitAsset.Colors.darkGray.swiftUIColor) + .frame(maxWidth: .infinity, maxHeight: 250) + } + } +} diff --git a/Apps/LekaUpdater/Sources/View/UpdatingViews/UpdateStatusView.swift b/Apps/LekaUpdater/Sources/View/UpdatingViews/UpdateStatusView.swift new file mode 100644 index 0000000000..d193809e55 --- /dev/null +++ b/Apps/LekaUpdater/Sources/View/UpdatingViews/UpdateStatusView.swift @@ -0,0 +1,104 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - UpdateStatusView + +struct UpdateStatusView: View { + @StateObject private var viewModel = UpdateStatusViewModel() + + @Binding var isConnectionViewPresented: Bool + + var body: some View { + VStack { + Spacer() + Spacer() + + VStack { + switch self.viewModel.updatingStatus { + case .sendingFile: + SendingFileIllustration() + case .rebootingRobot: + RebootingIllustration() + case .updateFinished: + UpdateFinishedIllustration() + case .error: + ErrorIllustration() + } + } + .frame(height: 250) + .padding(.bottom) + .padding(.bottom) + + if self.viewModel.updatingStatus == .error { + Text(l10n.update.errorTitle) + .font(.title) + .bold() + .padding() + } else { + Text(l10n.update.stepNumber("\(self.viewModel.stepNumber)/3")) + .font(.title) + .bold() + .monospacedDigit() + .padding() + .alert(isPresented: self.$viewModel.showAlert) { + Alert( + title: Text(l10n.update.alert.robotNotInChargeTitle), + message: Text(l10n.update.alert.robotNotInChargeMessage) + ) + } + } + + VStack { + switch self.viewModel.updatingStatus { + case .sendingFile: + SendingFileContentView(progress: self.$viewModel.sendingFileProgression) + case .rebootingRobot: + RebootingContentView() + case .updateFinished: + UpdateFinishedContentView(isConnectionViewPresented: self.$isConnectionViewPresented) + case .error: + ErrorContentView( + errorDescription: self.viewModel.errorDescription, + errorInstruction: self.viewModel.errorInstructions, + isConnectionViewPresented: self.$isConnectionViewPresented + ) + } + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: 250) + + Spacer() + + LekaUpdaterAsset.Assets.lekaUpdaterIcon.swiftUIImage + .resizable() + .scaledToFit() + .frame(height: 70) + .padding(35) + } + .foregroundColor(DesignKitAsset.Colors.darkGray.swiftUIColor) + .background(.lkBackground) + .onAppear(perform: self.viewModel.startUpdate) + .toolbar { + ToolbarItem(placement: .principal) { + VStack { + Text(l10n.main.appName) + .font(.title2) + .bold() + Text(l10n.main.appDescription) + } + .foregroundColor(.lkNavigationTitle) + } + } + } +} + +#Preview { + NavigationStack { + UpdateStatusView(isConnectionViewPresented: .constant(false)) + } +} diff --git a/Apps/LekaUpdater/Sources/ViewModel/InformationViewModel.swift b/Apps/LekaUpdater/Sources/ViewModel/InformationViewModel.swift new file mode 100644 index 0000000000..e8a66c8b90 --- /dev/null +++ b/Apps/LekaUpdater/Sources/ViewModel/InformationViewModel.swift @@ -0,0 +1,71 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import Foundation +import RobotKit +import Version + +class InformationViewModel: ObservableObject { + // MARK: Lifecycle + + init() { + self.subscribeToRobotNameUpdates() + self.subscribeToRobotOsVersionUpdates() + } + + // MARK: Public + + public func onViewReappear() { + self.robotName = Robot.shared.name.value + } + + // MARK: Internal + + @Published var showRobotCannotBeUpdated: Bool = false + @Published var showRobotNeedsUpdate: Bool = true + @Published var robotName: String = "n/a" + @Published var robotOSVersion: String = "" + + // MARK: Private + + private var cancellables: Set = [] + + private func subscribeToRobotNameUpdates() { + Robot.shared.name + .receive(on: DispatchQueue.main) + .sink { robotName in + self.robotName = robotName + } + .store(in: &self.cancellables) + } + + private func subscribeToRobotOsVersionUpdates() { + Robot.shared.osVersion + .receive(on: DispatchQueue.main) + .sink { robotOsVersion in + self.updateShowRobotCannotBeUpdated(robotOsVersion: robotOsVersion) + self.updateShowRobotNeedsUpdate(robotOsVersion: robotOsVersion) + self.robotOSVersion = robotOsVersion?.description ?? "(n/a)" + } + .store(in: &self.cancellables) + } + + private func updateShowRobotCannotBeUpdated(robotOsVersion: Version?) { + guard let robotOsVersion else { return } + + let isUpdateProcessAvailable = UpdateProcessController.availableVersions.contains(robotOsVersion) + self.showRobotCannotBeUpdated = !isUpdateProcessAvailable + } + + private func updateShowRobotNeedsUpdate(robotOsVersion: Version?) { + if let robotOsVersion { + let isUpdateProcessAvailable = UpdateProcessController.availableVersions.contains(robotOsVersion) + let isRobotNeedsUpdate = globalFirmwareManager.compareWith(version: robotOsVersion) == .needsUpdate + self.showRobotNeedsUpdate = isRobotNeedsUpdate && isUpdateProcessAvailable + } else { + self.showRobotNeedsUpdate = false + } + } +} diff --git a/Apps/LekaUpdater/Sources/ViewModel/RequirementsViewModel.swift b/Apps/LekaUpdater/Sources/ViewModel/RequirementsViewModel.swift new file mode 100644 index 0000000000..b25796e9a3 --- /dev/null +++ b/Apps/LekaUpdater/Sources/ViewModel/RequirementsViewModel.swift @@ -0,0 +1,67 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import Foundation +import LocalizationKit +import RobotKit +import SwiftUI + +class RequirementsViewModel: ObservableObject { + // MARK: Lifecycle + + init() { + self.subscribeToRobotBatteryUpdates() + self.subscribeToRobotIsChargingUpdates() + } + + // MARK: Internal + + let requirementsInstructionsText = l10n.update.requirements.instructionsText + + let chargingBasePluggedImage = LekaUpdaterAsset.Assets.chargingBasePlugged.swiftUIImage + let chargingBasePluggedText = l10n.update.requirements.chargingBasePluggedText + + let chargingBaseGreenLEDImage = LekaUpdaterAsset.Assets.chargingBaseGreenLED.swiftUIImage + let chargingBaseGreenLEDText = l10n.update.requirements.chargingBaseGreenLEDText + + let robotBatteryMinimumLevelImage = LekaUpdaterAsset.Assets.robotBatteryQuarter1.swiftUIImage + let robotBatteryMinimumLevelText = l10n.update.requirements.robotBatteryMinimumLevelText + + @Published var robotIsReadyToUpdate = false + @Published var robotIsNotReadyToUpdate = true + + // MARK: Private + + private var cancellables: Set = [] + + private func subscribeToRobotBatteryUpdates() { + Robot.shared.battery + .receive(on: DispatchQueue.main) + .sink { robotBattery in + self.updateRobotIsReadyToUpdate( + robotBattery: robotBattery, robotIsCharging: Robot.shared.isCharging.value + ) + } + .store(in: &self.cancellables) + } + + private func subscribeToRobotIsChargingUpdates() { + Robot.shared.isCharging + .receive(on: DispatchQueue.main) + .sink { robotIsCharging in + self.updateRobotIsReadyToUpdate( + robotBattery: Robot.shared.battery.value, robotIsCharging: robotIsCharging + ) + } + .store(in: &self.cancellables) + } + + private func updateRobotIsReadyToUpdate(robotBattery: Int?, robotIsCharging: Bool?) { + if let battery = robotBattery, let isCharging = robotIsCharging { + self.robotIsReadyToUpdate = battery >= 30 && isCharging + self.robotIsNotReadyToUpdate = !self.robotIsReadyToUpdate + } + } +} diff --git a/Apps/LekaUpdater/Sources/ViewModel/RobotInformationViewModel.swift b/Apps/LekaUpdater/Sources/ViewModel/RobotInformationViewModel.swift new file mode 100644 index 0000000000..ebc501a030 --- /dev/null +++ b/Apps/LekaUpdater/Sources/ViewModel/RobotInformationViewModel.swift @@ -0,0 +1,67 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import Foundation +import LocalizationKit +import RobotKit + +class RobotInformationViewModel: ObservableObject { + // MARK: Lifecycle + + init() { + self.subscribeToRobotSerialNumberUpdates() + self.subscribeToRobotBatteryUpdates() + self.subscribeToRobotOsVersionUpdates() + self.subscribeToRobotChargingStatusUpdates() + } + + // MARK: Internal + + @Published var robotSerialNumber = "n/a" + @Published var robotBattery = "n/a" + @Published var robotOsVersion = "n/a" + @Published var robotIsCharging = "n/a" + + // MARK: Private + + private var cancellables: Set = [] + + private func subscribeToRobotSerialNumberUpdates() { + Robot.shared.serialNumber + .receive(on: DispatchQueue.main) + .sink { robotSerialNumber in + self.robotSerialNumber = robotSerialNumber + } + .store(in: &self.cancellables) + } + + private func subscribeToRobotBatteryUpdates() { + Robot.shared.battery + .receive(on: DispatchQueue.main) + .sink { robotBattery in + self.robotBattery = "\(robotBattery)%" + } + .store(in: &self.cancellables) + } + + private func subscribeToRobotOsVersionUpdates() { + Robot.shared.osVersion + .receive(on: DispatchQueue.main) + .sink { robotOsVersion in + self.robotOsVersion = robotOsVersion?.description ?? "(n/a)" + } + .store(in: &self.cancellables) + } + + private func subscribeToRobotChargingStatusUpdates() { + Robot.shared.isCharging + .receive(on: DispatchQueue.main) + .sink { isCharging in + self.robotIsCharging = + isCharging ? String(l10n.general.yes.characters) : String(l10n.general.no.characters) + } + .store(in: &self.cancellables) + } +} diff --git a/Apps/LekaUpdater/Sources/ViewModel/UpdateStatusViewModel.swift b/Apps/LekaUpdater/Sources/ViewModel/UpdateStatusViewModel.swift new file mode 100644 index 0000000000..10c4775542 --- /dev/null +++ b/Apps/LekaUpdater/Sources/ViewModel/UpdateStatusViewModel.swift @@ -0,0 +1,144 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import Foundation +import LocalizationKit +import RobotKit +import SwiftUI + +class UpdateStatusViewModel: ObservableObject { + // MARK: Lifecycle + + init() { + self.subscribeToStateUpdates() + self.subscribeToSendingFileProgressionUpdates() + self.subscribeToRobotIsChargingUpdates() + } + + // MARK: Public + + // MARK: - Public variables + + @Published public var updatingStatus: UpdateStatus = .sendingFile + @Published public var sendingFileProgression: Float = 0.0 + @Published public var showAlert: Bool = false + + @Published public var errorDescription: String = "" + @Published public var errorInstructions: String = "" + + public var stepNumber: Int { + switch self.updatingStatus { + case .sendingFile: + 1 + case .rebootingRobot: + 2 + case .updateFinished: + 3 + case .error: + -1 + } + } + + public func startUpdate() { + UIApplication.shared.isIdleTimerDisabled = true + + self.updateProcessController.startUpdate() + } + + // MARK: Internal + + enum UpdateStatus { + case sendingFile + case rebootingRobot + case updateFinished + case error + } + + // MARK: Private + + // MARK: - Private variables + + private var updateProcessController = UpdateProcessController() + + private var cancellables: Set = [] + + private func subscribeToStateUpdates() { + self.updateProcessController.currentStage + .receive(on: DispatchQueue.main) + .sink { completion in + self.showAlert = false + + switch completion { + case .finished: + self.updatingStatus = .updateFinished + case let .failure(error): + self.updatingStatus = .error + + switch error { + case .failedToLoadFile: + self.errorDescription = String(l10n.update.error.failedToLoadFileDescription.characters) + self.errorInstructions = String( + l10n.update.error.failedToLoadFileInstructions.characters) + + case .robotNotUpToDate: + self.errorDescription = String(l10n.update.error.robotNotUpToDateDescription.characters) + self.errorInstructions = String( + l10n.update.error.robotNotUpToDateInstructions.characters) + + case .updateProcessNotAvailable: + self.errorDescription = String( + l10n.update.error.updateProcessNotAvailableDescription.characters) + self.errorInstructions = String( + l10n.update.error.updateProcessNotAvailableInstructions.characters) + + case .robotUnexpectedDisconnection: + self.errorDescription = String( + l10n.update.error.robotUnexpectedDisconnectionDescription.characters) + self.errorInstructions = String( + l10n.update.error.robotUnexpectedDisconnectionInstructions.characters) + + default: + self.errorDescription = String(l10n.update.error.unknownErrorDescription.characters) + self.errorInstructions = String(l10n.update.error.unknownErrorInstructions.characters) + } + } + self.onUpdateEnded() + } receiveValue: { state in + switch state { + case .initial, + .sendingUpdate: + self.updatingStatus = .sendingFile + case .installingUpdate: + self.updatingStatus = .rebootingRobot + } + } + .store(in: &self.cancellables) + } + + private func subscribeToSendingFileProgressionUpdates() { + self.updateProcessController.sendingFileProgression + .receive(on: DispatchQueue.main) + .sink(receiveValue: { progression in + self.sendingFileProgression = progression + }) + .store(in: &self.cancellables) + } + + private func subscribeToRobotIsChargingUpdates() { + Robot.shared.isCharging + .receive(on: DispatchQueue.main) + .sink { robotIsCharging in + let robotShouldBeInCharge = + self.updatingStatus == .sendingFile || self.updatingStatus == .rebootingRobot + + self.showAlert = robotShouldBeInCharge && robotIsCharging == false + } + .store(in: &self.cancellables) + } + + private func onUpdateEnded() { + UIApplication.shared.isIdleTimerDisabled = false + } +} diff --git a/Apps/LekaUpdater/Tests/FirmwareManager_Tests.swift b/Apps/LekaUpdater/Tests/FirmwareManager_Tests.swift new file mode 100644 index 0000000000..842352263f --- /dev/null +++ b/Apps/LekaUpdater/Tests/FirmwareManager_Tests.swift @@ -0,0 +1,63 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Version +import XCTest + +@testable import LekaUpdater + +// MARK: - FirmwareManager_Tests_compareVersion + +final class FirmwareManager_Tests_compareVersion: XCTestCase { + func test_shouldReturnRobotNeedUpdate_lowerVersion() { + let firmwareManager = FirmwareManager() + let robotVersion = Version(1, 0, 0) + + let expected = RobotUpdateStatus.needsUpdate + let actual = firmwareManager.compareWith(version: robotVersion) + XCTAssertEqual(expected, actual) + } + + func test_shouldReturnRobotIsUpToDate_sameVersion() { + let firmwareManager = FirmwareManager() + let robotVersion = firmwareManager.currentVersion + + let expected = RobotUpdateStatus.upToDate + let actual = firmwareManager.compareWith(version: robotVersion) + XCTAssertEqual(expected, actual) + } + + func test_shouldReturnRobotIsUpToDate_higherVersion() { + let firmwareManager = FirmwareManager() + let robotVersion = Version(99, 99, 999) + + let expected = RobotUpdateStatus.upToDate + let actual = firmwareManager.compareWith(version: robotVersion) + XCTAssertEqual(expected, actual) + } + + func test_shouldReturnRobotNeedUpdate_invalidVersion() { + let firmwareManager = FirmwareManager() + let robotVersion = Version("⚠️ NO OS VERSION") + + let expected = RobotUpdateStatus.needsUpdate + let actual = firmwareManager.compareWith(version: robotVersion) + XCTAssertEqual(expected, actual) + } +} + +// MARK: - FirmwareManager_Tests_sha256 + +final class FirmwareManager_Tests_sha256: XCTestCase { + func test_shouldReturnRobotNeedUpdate_invalidVersion() { + let firmwareManager = FirmwareManager() + _ = firmwareManager.load() + + // shasum -a 256 LekaOS-1.4.0.bin + let expectedSHA256 = "e061ee13041fe73667e4e9ca8f84189fb4cbc8f3d8710f8841fb41cf0df9e1e1" + let actualSHA256 = firmwareManager.sha256 + + XCTAssertEqual(expectedSHA256, actualSHA256) + } +} diff --git a/Apps/LekaUpdater/Tests/LekaUpdaterTests.swift b/Apps/LekaUpdater/Tests/LekaUpdaterTests.swift deleted file mode 100644 index 98aa3dc83d..0000000000 --- a/Apps/LekaUpdater/Tests/LekaUpdaterTests.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation -import XCTest - -final class LekaUpdaterTests: XCTestCase { - func test_twoPlusTwo_isFour() { - XCTAssertEqual(2+2, 4) - } -} diff --git a/Apps/LekaUpdater/Tests/LekaUpdater_Tests.swift b/Apps/LekaUpdater/Tests/LekaUpdater_Tests.swift new file mode 100644 index 0000000000..ee0f2e21ad --- /dev/null +++ b/Apps/LekaUpdater/Tests/LekaUpdater_Tests.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +final class LekaUpdater_Tests: XCTestCase { + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/Documentation/BLEKit.md b/Documentation/BLEKit.md new file mode 100644 index 0000000000..8de996b83d --- /dev/null +++ b/Documentation/BLEKit.md @@ -0,0 +1,91 @@ +# BLEKit Overview + +## BLESpecs + +Struct classifying each of the services and their characteristics using `CBUUID` type. +The features and services can be invoked in all code importing the `BLEKit` module in the respective form of: + +- `BLESpecs..Characteristic.` +- `BLESpecs..service` + +**For example:** the `level` feature of the `Battery` service. + +```swift +public struct Battery { + public static let service = CBUUID(string: "0x180F") + + public structCharacteristics { + public static let level = CBUUID(string: "0x2A19") + } +} +``` + +## AdvertisingData + +Struct that initializes and formats the advertising data of the `peripheral` discovered by the `BLEManager`. +The advertising data supported by our `BLEKit`, and therefore displayable, are: + +- `name` +- `battery` - `0 --> 100%` +- `isCharging` - `true` or `false` +- `osVersion` - `x.y.z` + +The advertising data of a `peripheral` is obtained by instantiating an `AdvertisingData` with, as argument, the `advertisingData` of this peripheral: `AdvertisingData(peripheral.advertisementData)`. + +## Functioning + +### BLEManager + +The BLEKit module was inspired by the **CombineCoreBluetooth** repo [[Link]](https://github.com/StarryInternet/CombineCoreBluetooth), using the **Combine** framework ([Documentation/Combine](https:// developer.apple.com/documentation/combine). + +`BLEManager` is what is called the `central`. + +`BLEManager` handles: + +- **discovery** of `peripherals`: `searchForPeripheral()` to start the scan, `stopSearching()` to stop it. The `isScanning` member variable allows to know the current state of the `central` (`true` if scanning is activated) +- **connection** to one of them: `connect(peripheral:)` to connect to a particular `peripheral`. The `peripheral` in question can be found in the list of `peripherals` discovered by `BLEManager` +- **disconnection**: `disconnect()` disconnects the currently connected `peripheral` + +### Combine + +The **Combine** framework provides a Swift API for **processing values over time**. These values can represent many types of asynchronous events. **Combine** declares `Publishers` to publish values to change over time, and `Subscribers` to receive these values. +Many functions of **CombineCoreBluetooth** return publishers. To understand how to use them, let's detail an example of use made for the connection: + +The `connect()` function, returning an `AnyPublisher`, which publishes a `Peripheral` to it on a successful connection and an error on a failed connection. + +```swift +bleManager.connect(bleManager.peripherals[botVM.currentlySelectedBotIndex!]) + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { _ in }, + receiveValue: { peripheral in + let connectedRobotPeripheral = RobotPeripheral(peripheral: peripheral) + robot.robotPeripheral = connectedRobotPeripheral + }) + .store(in: &bleManager.cancellables) +``` + +`receive(on: DispatchQueue.main)` forces the publisher to publish to the `MainThread` (to be used by default when working on a publisher that will impact interfaces) + +`sink( receiveCompletion: { _ in }, receiveValue: { _ in }` attaches a `Subscriber` to our `Publisher` created just above and will perform the action explained in the `receiveValue()` section when receipt of data from the `Publisher`. +Here, we instance a `RobotPeripheral` with the robot to which we have just connected, then we update the `robotPeripheral` of our `robot` object. + +`store()` stores the output of `sink()` in a set of cancelables, destroyed when code execution stops. + +### RobotPeripheral + +`RobotPeripheral` is what is called the `peripheral`. Here, it is specific to a robot and is able to: + +- the **discovery** `characteristics` and the **notification** of an update of their value: `discoverAndListenCharacteristics()` discovers the `characteristics` among the list of `notifyingCharacteristics` (details later) and notify when a value change on these `characteristics`. +- the **voluntary** reading** of `characteristics`: `readReadOnlyCharacteristics()` reads the value of the characteristic in question +- **sending command** on a `characteristics`: `sendCommand(data:)` writes the data on the **characteristic `DFB0` specifically**. + +To go further: functions close to those of sendCommand could be created in order to ensure the writing on other `characteristics`. + +### Robot class (upcoming evolution) + +`Robot` is a class that allows communication between the `BLEKit` module and the **front**. This class has, as a member variable, the values associated with the characteristics of the robot, in a format that can be displayed by the UI. Robot does the following: + +- **specify the formatting** for each value of the characteristics: `registerReadOnlyCharacteristicClosures()` and `registerNotifyingCharacteristicClosures()` gives the procedure for decoding the data read for each `characteristic`, respectively for those read voluntarily, and those notifying . +- **create a link with the RobotPeripheral**. Robot has as an optional member variable a RobotPeripheral, which when the latter is instantiated, we call the `updateDeviceInformation()` and `subscribeToDataUpdates()` functions which will respectively ensure the reading of the characteristics of the `DeviceInformation` service and the listening of ` characteristics that change over time such as battery level, charging status, or MagicCard information +- **run a reinforcer**: `runReinforcer(reinforcer:)`. More generally, for the sequel, any command found in the `Commands.swift*` file can be sent and played by adapting the new function to the specific command. diff --git a/Examples/Module/Examples/ModuleExampleAppOne/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/Module/Examples/ModuleExampleAppOne/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Examples/Module/Examples/ModuleExampleAppOne/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Module/Examples/ModuleExampleAppOne/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/Module/Examples/ModuleExampleAppOne/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Examples/Module/Examples/ModuleExampleAppOne/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Module/Examples/ModuleExampleAppOne/Resources/Assets.xcassets/Contents.json b/Examples/Module/Examples/ModuleExampleAppOne/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/Module/Examples/ModuleExampleAppOne/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Module/Examples/ModuleExampleAppOne/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Examples/Module/Examples/ModuleExampleAppOne/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/Module/Examples/ModuleExampleAppOne/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Module/Examples/ModuleExampleAppOne/Sources/ContentView.swift b/Examples/Module/Examples/ModuleExampleAppOne/Sources/ContentView.swift new file mode 100644 index 0000000000..c0c799daf5 --- /dev/null +++ b/Examples/Module/Examples/ModuleExampleAppOne/Sources/ContentView.swift @@ -0,0 +1,22 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Module +import SwiftUI + +// MARK: - ContentView + +struct ContentView: View { + var body: some View { + HelloView(color: .mint, name: "Module Example App One") + } +} + +// MARK: - ContentView_Previews + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/Examples/Module/Examples/ModuleExampleAppOne/Sources/MainApp.swift b/Examples/Module/Examples/ModuleExampleAppOne/Sources/MainApp.swift new file mode 100644 index 0000000000..1ba904e32b --- /dev/null +++ b/Examples/Module/Examples/ModuleExampleAppOne/Sources/MainApp.swift @@ -0,0 +1,14 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +@main +struct ModuleExampleAppOne: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/Examples/Module/Examples/ModuleExampleAppTwo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/Module/Examples/ModuleExampleAppTwo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Examples/Module/Examples/ModuleExampleAppTwo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Module/Examples/ModuleExampleAppTwo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/Module/Examples/ModuleExampleAppTwo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Examples/Module/Examples/ModuleExampleAppTwo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Module/Examples/ModuleExampleAppTwo/Resources/Assets.xcassets/Contents.json b/Examples/Module/Examples/ModuleExampleAppTwo/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/Module/Examples/ModuleExampleAppTwo/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Module/Examples/ModuleExampleAppTwo/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Examples/Module/Examples/ModuleExampleAppTwo/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/Module/Examples/ModuleExampleAppTwo/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Module/Examples/ModuleExampleAppTwo/Sources/ContentView.swift b/Examples/Module/Examples/ModuleExampleAppTwo/Sources/ContentView.swift new file mode 100644 index 0000000000..67ab6ec095 --- /dev/null +++ b/Examples/Module/Examples/ModuleExampleAppTwo/Sources/ContentView.swift @@ -0,0 +1,24 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Module +import SwiftUI + +// MARK: - ContentView + +struct ContentView: View { + let appName = ModuleExampleAppTwoResources.bundle.infoDictionary?["APP_NAME"] as? String ?? "NO NAME" + + var body: some View { + HelloView(color: .teal, name: self.appName) + } +} + +// MARK: - ContentView_Previews + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/Examples/Module/Examples/ModuleExampleAppTwo/Sources/MainApp.swift b/Examples/Module/Examples/ModuleExampleAppTwo/Sources/MainApp.swift new file mode 100644 index 0000000000..f4d0c9f1ef --- /dev/null +++ b/Examples/Module/Examples/ModuleExampleAppTwo/Sources/MainApp.swift @@ -0,0 +1,14 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +@main +struct ModuleExampleAppTwo: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/Examples/Module/Project.swift b/Examples/Module/Project.swift new file mode 100644 index 0000000000..2891f9748e --- /dev/null +++ b/Examples/Module/Project.swift @@ -0,0 +1,24 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.module( + name: "Module", + examples: [ + ModuleExample(name: "ModuleExampleAppOne"), + ModuleExample( + name: "ModuleExampleAppTwo", + infoPlist: [ + "APP_NAME": "ModuleExampleAppTwo from InfoPlist", + ] + ), + ], + dependencies: [ + // no deps + ] +) diff --git a/Examples/Module/Resources/Assets.xcassets/Contents.json b/Examples/Module/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/Module/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Module/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Examples/Module/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/Module/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Module/Sources/HelloView.swift b/Examples/Module/Sources/HelloView.swift new file mode 100644 index 0000000000..1957354f6d --- /dev/null +++ b/Examples/Module/Sources/HelloView.swift @@ -0,0 +1,44 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// MARK: - HelloView + +public struct HelloView: View { + // MARK: Lifecycle + + public init(color: Color, name: String) { + self.color = color + self.name = name + } + + // MARK: Public + + public var body: some View { + ZStack { + self.color + .ignoresSafeArea() + + Text("Hello, \(self.name)!") + .foregroundColor(.white) + .font(.largeTitle) + .fontWeight(.bold) + .multilineTextAlignment(.center) + } + } + + // MARK: Internal + + var color: Color + var name: String +} + +// MARK: - ContentView_Previews + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + HelloView(color: .blue, name: "Dynamic Library Module Example") + } +} diff --git a/Examples/Module/Tests/Module_Tests.swift b/Examples/Module/Tests/Module_Tests.swift new file mode 100644 index 0000000000..30201a508d --- /dev/null +++ b/Examples/Module/Tests/Module_Tests.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +final class Module_Tests: XCTestCase { + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/Examples/iOSApp/Project.swift b/Examples/iOSApp/Project.swift new file mode 100644 index 0000000000..e39936b105 --- /dev/null +++ b/Examples/iOSApp/Project.swift @@ -0,0 +1,15 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.app( + name: "iOSApp", + dependencies: [ + .project(target: "Module", path: Path("../../Examples/Module")), + ] +) diff --git a/Examples/iOSApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/iOSApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Examples/iOSApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/iOSApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/iOSApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Examples/iOSApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/iOSApp/Resources/Assets.xcassets/Contents.json b/Examples/iOSApp/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/iOSApp/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/iOSApp/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Examples/iOSApp/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/iOSApp/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/iOSApp/Sources/ContentView.swift b/Examples/iOSApp/Sources/ContentView.swift new file mode 100644 index 0000000000..d5bf78f131 --- /dev/null +++ b/Examples/iOSApp/Sources/ContentView.swift @@ -0,0 +1,22 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Module +import SwiftUI + +// MARK: - ContentView + +struct ContentView: View { + var body: some View { + HelloView(color: .mint, name: "iOS App Example") + } +} + +// MARK: - ContentView_Previews + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/Examples/iOSApp/Sources/MainApp.swift b/Examples/iOSApp/Sources/MainApp.swift new file mode 100644 index 0000000000..512439ca04 --- /dev/null +++ b/Examples/iOSApp/Sources/MainApp.swift @@ -0,0 +1,14 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +@main +struct iOSApp: App { // swiftlint:disable:this type_name + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/Examples/iOSApp/Tests/iOSApp_Tests.swift b/Examples/iOSApp/Tests/iOSApp_Tests.swift new file mode 100644 index 0000000000..ad85b69fc6 --- /dev/null +++ b/Examples/iOSApp/Tests/iOSApp_Tests.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +final class iOSApp_Tests: XCTestCase { // swiftlint:disable:this type_name + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/Examples/macOSApp/Project.swift b/Examples/macOSApp/Project.swift new file mode 100644 index 0000000000..4ab8c4a9e4 --- /dev/null +++ b/Examples/macOSApp/Project.swift @@ -0,0 +1,16 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.app( + name: "macOSApp", + deploymentTargets: .macOS("13.0"), + dependencies: [ + .project(target: "Module", path: Path("../../Examples/Module")), + ] +) diff --git a/Examples/macOSApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/macOSApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Examples/macOSApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/macOSApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/macOSApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Examples/macOSApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/macOSApp/Resources/Assets.xcassets/Contents.json b/Examples/macOSApp/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/macOSApp/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/macOSApp/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Examples/macOSApp/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/macOSApp/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/macOSApp/Sources/ContentView.swift b/Examples/macOSApp/Sources/ContentView.swift new file mode 100644 index 0000000000..f77f85f67f --- /dev/null +++ b/Examples/macOSApp/Sources/ContentView.swift @@ -0,0 +1,22 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Module +import SwiftUI + +// MARK: - ContentView + +struct ContentView: View { + var body: some View { + HelloView(color: .teal, name: "macOS App Example") + } +} + +// MARK: - ContentView_Previews + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/Examples/macOSApp/Sources/MainApp.swift b/Examples/macOSApp/Sources/MainApp.swift new file mode 100644 index 0000000000..77ddba2b9a --- /dev/null +++ b/Examples/macOSApp/Sources/MainApp.swift @@ -0,0 +1,14 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +@main +struct macOSApp: App { // swiftlint:disable:this type_name + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/Examples/macOSApp/Tests/macOSApp_Tests.swift b/Examples/macOSApp/Tests/macOSApp_Tests.swift new file mode 100644 index 0000000000..9663a054bc --- /dev/null +++ b/Examples/macOSApp/Tests/macOSApp_Tests.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +final class macOSApp_Tests: XCTestCase { // swiftlint:disable:this type_name + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/Examples/macOSCli/Project.swift b/Examples/macOSCli/Project.swift new file mode 100644 index 0000000000..d97a26f731 --- /dev/null +++ b/Examples/macOSCli/Project.swift @@ -0,0 +1,16 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.cli( + name: "macOSCli", + version: "1.0.0", + dependencies: [ + .external(name: "ArgumentParser"), + ] +) diff --git a/Examples/macOSCli/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/macOSCli/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Examples/macOSCli/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/macOSCli/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/macOSCli/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Examples/macOSCli/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/macOSCli/Resources/Assets.xcassets/Contents.json b/Examples/macOSCli/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/macOSCli/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/macOSCli/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Examples/macOSCli/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/macOSCli/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/macOSCli/Sources/MainApp.swift b/Examples/macOSCli/Sources/MainApp.swift new file mode 100644 index 0000000000..1531427674 --- /dev/null +++ b/Examples/macOSCli/Sources/MainApp.swift @@ -0,0 +1,29 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ArgumentParser + +@main +struct Greetings: ParsableCommand { + @Flag(help: "Include a counter with each repetition.") + var includeCounter = false + + @Option(name: .shortAndLong, help: "The number of times to great 'name'.") + var count: Int? + + @Argument(help: "The phrase to repeat.") + var name: String = "macOS Cli Example!" + + mutating func run() throws { + let repeatCount = self.count ?? 2 + + for i in 1...repeatCount { + if self.includeCounter { + print("\(i): Hello, \(self.name)") + } else { + print("Hello, \(self.name)") + } + } + } +} diff --git a/Examples/macOSCli/Tests/macOSCli_Tests.swift b/Examples/macOSCli/Tests/macOSCli_Tests.swift new file mode 100644 index 0000000000..c4e376783f --- /dev/null +++ b/Examples/macOSCli/Tests/macOSCli_Tests.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +final class macOSCli_Tests: XCTestCase { // swiftlint:disable:this type_name + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000000..045e13f43f --- /dev/null +++ b/Gemfile @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +gem 'fastlane', '~>2.219.0' + +gem 'rubocop' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000000..5c2abb408e --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,242 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) + artifactory (3.0.15) + ast (2.4.2) + atomos (0.1.3) + aws-eventstream (1.3.0) + aws-partitions (1.894.0) + aws-sdk-core (3.191.3) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.8) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.77.0) + aws-sdk-core (~> 3, >= 3.191.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.143.0) + aws-sdk-core (~> 3, >= 3.191.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.2.0) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.109.0) + faraday (1.10.3) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) + faraday (~> 1.0) + fastimage (2.3.0) + fastlane (2.219.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (>= 0.1.1) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.6.1) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.3.1) + google-cloud-storage (1.47.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.5) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.6.2) + json (2.7.1) + jwt (2.8.0) + base64 + language_server-protocol (3.17.0.3) + mini_magick (4.12.0) + mini_mime (1.1.5) + multi_json (1.15.0) + multipart-post (2.4.0) + nanaimo (0.3.0) + naturally (2.2.1) + nkf (0.2.0) + optparse (0.4.0) + os (1.1.4) + parallel (1.24.0) + parser (3.3.0.5) + ast (~> 2.4.1) + racc + plist (3.7.1) + public_suffix (5.0.4) + racc (1.7.3) + rainbow (3.1.1) + rake (13.1.0) + regexp_parser (2.9.0) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.6) + rouge (2.0.7) + rubocop (1.60.2) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.30.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.30.0) + parser (>= 3.2.1.0) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.3) + signet (0.19.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.5.0) + word_wrap (1.0.0) + xcodeproj (1.24.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + rexml (~> 3.2.4) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + arm64-darwin-22 + arm64-darwin-23 + +DEPENDENCIES + fastlane (~> 2.219.0) + rubocop + +BUNDLED WITH + 2.5.4 diff --git a/LICENSE b/LICENSE index 820da5964c..249f1a23bc 100644 --- a/LICENSE +++ b/LICENSE @@ -198,4 +198,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..743f5b0cc5 --- /dev/null +++ b/Makefile @@ -0,0 +1,89 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +# +# MARK: - Options +# + +GENERATE_MODULES_AS_FRAMEWORKS_FOR_DEBUG ?= TRUE +TURN_OFF_LINTERS ?= FALSE +TEST_FLIGHT_APP_NAME ?= LekaActivityUIExplorer + + +# +# MARK: - Build targets +# + +fetch: + @echo "Fetching dependencies..." + @tuist fetch + +config: + @echo "Generating project..." + @TUIST_GENERATE_MODULES_AS_FRAMEWORKS_FOR_DEBUG=$(GENERATE_MODULES_AS_FRAMEWORKS_FOR_DEBUG) \ + TUIST_TURN_OFF_LINTERS=$(TURN_OFF_LINTERS) \ + tuist generate + +build: + @echo "Building project..." + @TUIST_GENERATE_MODULES_AS_FRAMEWORKS_FOR_DEBUG=$(GENERATE_MODULES_AS_FRAMEWORKS_FOR_DEBUG) \ + TUIST_TURN_OFF_LINTERS=$(TURN_OFF_LINTERS) \ + tuist build + +clean: + @echo "Cleaning project..." + @tuist clean + @rm -rf .build + @rm -rf ~/Library/Developer/Xcode/DerivedData + @gfind . -type d -name "*.xcodeproj" -exec rm -rf {} + + + +# +# MARK: - Tools targets +# + +setup: + @echo "Setting up dev environment..." + @brew update && brew upgrade + @brew install swiftformat swiftlint fastlane + @brew install tuist --no-quarantine + +sync_certificates: + @echo "Syncing certificates..." + @export FASTLANE_SKIP_UPDATE_CHECK=1 + @fastlane sync_certificates + +git_hooks: + @echo "Installing pre-commit hooks..." + @brew install pre-commit + @pre-commit install + +git_tools: + @echo "Installing git tools..." + @brew install gh git-lfs + @git lfs install + @brew install node && npm install --global git-json-merge + @echo "node and git-json-merge have been installed, please run the following:" + @echo " git config merge.json.driver \"git-json-merge %A %O %B\"" + @echo " git config merge.json.name \"custom merge driver for json files\"" + +format: + @echo "Formatting code..." + @swiftlint --quiet --fix + @swiftformat . + +lint: + @echo "Linting code..." + @-swiftlint --quiet --fix && swiftlint --quiet --progress + @echo "" + @-swiftformat --lint . + +ci_test_flight_release: + @git checkout main + @git pull + @git checkout release/testflight-beta + @git rebase main + @git push --force-with-lease + @gh pr edit --add-label "fastlane:rbi $(TEST_FLIGHT_APP_NAME)" + @git checkout main diff --git a/Modules/AccountKit/AccountKitExample.entitlements b/Modules/AccountKit/AccountKitExample.entitlements new file mode 100644 index 0000000000..a812db506f --- /dev/null +++ b/Modules/AccountKit/AccountKitExample.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.applesignin + + Default + + + diff --git a/Modules/AccountKit/Examples/AccountKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Modules/AccountKit/Examples/AccountKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Modules/AccountKit/Examples/AccountKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/AccountKit/Examples/AccountKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Modules/AccountKit/Examples/AccountKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Modules/AccountKit/Examples/AccountKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/AccountKit/Examples/AccountKitExample/Resources/Assets.xcassets/Contents.json b/Modules/AccountKit/Examples/AccountKitExample/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/AccountKit/Examples/AccountKitExample/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/AccountKit/Examples/AccountKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Modules/AccountKit/Examples/AccountKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/AccountKit/Examples/AccountKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/AccountKit/Examples/AccountKitExample/Sources/MainApp.swift b/Modules/AccountKit/Examples/AccountKitExample/Sources/MainApp.swift new file mode 100644 index 0000000000..d03f75cd12 --- /dev/null +++ b/Modules/AccountKit/Examples/AccountKitExample/Sources/MainApp.swift @@ -0,0 +1,15 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import SwiftUI + +@main +struct AccountKitExample: App { + var body: some Scene { + WindowGroup { + MainView() + } + } +} diff --git a/Modules/AccountKit/Examples/AccountKitExample/Sources/MainView.swift b/Modules/AccountKit/Examples/AccountKitExample/Sources/MainView.swift new file mode 100644 index 0000000000..951b7e34b9 --- /dev/null +++ b/Modules/AccountKit/Examples/AccountKitExample/Sources/MainView.swift @@ -0,0 +1,37 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AccountKit +import SwiftUI + +struct MainView: View { + var body: some View { + Text("Hello, AccountKit!") + .onAppear { + let professions = Professions.list + for (index, profession) in professions.enumerated() { + print("index: \(index + 1)") + print("version: \(Professions.version)") + print("id: \(profession.id)") + print("name: \(profession.name)") + print("description: \(profession.description)") + } + + let avatarCategories = Avatars.categories + for (index, avatarCategorie) in avatarCategories.enumerated() { + print("index: \(index + 1)") + print("version: \(Avatars.version)") + print("id: \(avatarCategorie.id)") + print("name: \(avatarCategorie.name)") + for icon in avatarCategorie.avatars { + print("iconName: \(icon)") + } + } + } + } +} + +#Preview { + MainView() +} diff --git a/Modules/AccountKit/Project.swift b/Modules/AccountKit/Project.swift new file mode 100644 index 0000000000..5ecef504f7 --- /dev/null +++ b/Modules/AccountKit/Project.swift @@ -0,0 +1,31 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.module( + name: "AccountKit", + examples: [ + ModuleExample( + name: "AccountKitExample" + ), + ], + dependencies: [ + .project(target: "DesignKit", path: Path("../../Modules/DesignKit")), + .project(target: "LogKit", path: Path("../../Modules/LogKit")), + .project(target: "LocalizationKit", path: Path("../../Modules/LocalizationKit")), + .project(target: "RobotKit", path: Path("../../Modules/RobotKit")), + .external(name: "FirebaseAnalytics"), + .external(name: "FirebaseAuth"), + .external(name: "FirebaseAuthCombine-Community"), + .external(name: "FirebaseFirestore"), + .external(name: "FirebaseFirestoreSwift"), + .external(name: "FirebaseFirestoreCombine-Community"), + .external(name: "Version"), + .external(name: "Yams"), + ] +) diff --git a/Modules/AccountKit/Resources/Assets.xcassets/Contents.json b/Modules/AccountKit/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/AccountKit/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/AccountKit/Resources/Localizable.xcstrings b/Modules/AccountKit/Resources/Localizable.xcstrings new file mode 100644 index 0000000000..d472b5297d --- /dev/null +++ b/Modules/AccountKit/Resources/Localizable.xcstrings @@ -0,0 +1,96 @@ +{ + "version": "1.0", + "sourceLanguage": "en", + "strings": { + "accountkit.auth_manager.signin_failed_error": { + "comment": "Sign-in failure error message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Sign-in failed. Please try again." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La connexion a \u00e9chou\u00e9. Veuillez r\u00e9essayer." + } + } + } + }, + "accountkit.auth_manager.signout_failed_error": { + "comment": "Sign-out failure error message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Failed to sign out. Please try again." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La d\u00e9connexion a \u00e9chou\u00e9. Veuillez r\u00e9essayer." + } + } + } + }, + "accountkit.auth_manager.signup_failed_error": { + "comment": "Sign-up failure error message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Sign-up failed. Please try again later." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L'inscription a \u00e9chou\u00e9. Veuillez r\u00e9essayer plus tard." + } + } + } + }, + "accountkit.auth_manager.verification_email_failed_error": { + "comment": "Verification email failure error message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "There was an error sending the verification email. Please try again later." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une erreur s'est produite lors de l'envoi de l'e-mail de v\u00e9rification. Veuillez r\u00e9essayer plus tard." + } + } + } + }, + "accountkit.auth_manager_view_model.unverified_email_notification": { + "comment": "Unverified email notification message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Your email hasn't been verified yet. Please verify your email to avoid losing your data." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous n'avez pas confirm\u00e9 votre email. Veuillez le v\u00e9rifier afin d'\u00e9viter de perdre vos donn\u00e9es." + } + } + } + } + } +} diff --git a/Modules/AccountKit/Resources/avatars/avatars.yml b/Modules/AccountKit/Resources/avatars/avatars.yml new file mode 100644 index 0000000000..67c89279ce --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/avatars.yml @@ -0,0 +1,140 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 +categories: + - id: weather + visible: true + l10n: + - locale: fr_FR + name: Météo + - locale: en_US + name: Weather + avatars: + - weather_cloud_with_face + - weather_snowflake_with_face + - weather_moon_with_face + - weather_star_with_face + - weather_sun_with_face + + - id: professions + visible: true + l10n: + - locale: fr_FR + name: Métiers + - locale: en_US + name: Professions + avatars: + - professions_astronaut_leka + - professions_cook_leka + - professions_doctor_leka + - professions_explorer_leka + - professions_sailor_leka + - professions_pirate_leka + + - id: eyeglasses + visible: true + l10n: + - locale: fr_FR + name: Lunettes + - locale: en_US + name: Eyeglasses + avatars: + - eyeglasses_blue_leka + - eyeglasses_green_leka + - eyeglasses_pink_leka + - eyeglasses_yellow_leka + + - id: animals + visible: true + l10n: + - locale: fr_FR + name: Animaux + - locale: en_US + name: Animals + avatars: + - animals_bird + - animals_horse + - animals_rooster + - animals_bear + - animals_fox + - animals_hedgehog + - animals_rabbit + - animals_squirrel + - animals_fish + - animals_turtle + - animals_elephant + - animals_giraffe + - animals_koala + - animals_lion + - animals_crab + - animals_turtle + + - id: fruits + visible: true + l10n: + - locale: fr_FR + name: Fruits + - locale: en_US + name: Fruits + avatars: + - fruits_apple_green + - fruits_apple_red + - fruits_banana + - fruits_cherry + - fruits_grape + - fruits_kiwi + - fruits_lemon + - fruits_pear + - fruits_pineapple + - fruits_strawberry + - fruits_watermelon + + - id: vegetables + visible: true + l10n: + - locale: fr_FR + name: Légumes + - locale: en_US + name: Vegetables + avatars: + - vegetables_avocado + - vegetables_broccoli + - vegetables_carrot + - vegetables_corn + - vegetables_eggplant + - vegetables_onion + - vegetables_potato + - vegetables_salad + - vegetables_tomato + + - id: alpha + visible: false + l10n: + - locale: fr_FR + name: Alpha + - locale: en_US + name: Alpha + avatars: + - anonymous + - boy1 + - boy2 + - boy3 + - boy4 + - boy5 + - boy6 + - boy7 + - boy8 + - boy9 + - boy10 + - girl1 + - girl2 + - girl3 + - girl4 + - girl5 + - girl6 + - girl7 + - girl8 + - girl9 + - girl10 diff --git a/Modules/AccountKit/Resources/avatars/images/animals_bear.avatars.png b/Modules/AccountKit/Resources/avatars/images/animals_bear.avatars.png new file mode 100755 index 0000000000..22a8cb846f --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/animals_bear.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57a468f31c8ebd630cff24de6a898cdea2088093a32453e15bc11371b91c0cbf +size 9779 diff --git a/Modules/AccountKit/Resources/avatars/images/animals_bird.avatars.png b/Modules/AccountKit/Resources/avatars/images/animals_bird.avatars.png new file mode 100755 index 0000000000..9d5b71aa14 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/animals_bird.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b3cfb34d98607e65dc81f1cec3d1d30b878e2bb7b3547ae1ad6571dff39f5d6 +size 9475 diff --git a/Modules/AccountKit/Resources/avatars/images/animals_crab.avatars.png b/Modules/AccountKit/Resources/avatars/images/animals_crab.avatars.png new file mode 100755 index 0000000000..3ab01ff1e7 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/animals_crab.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54a1351a5e073f2133e4e1ea7f053a4bf51c1f8287788e1c304a62fdf8e3cf30 +size 21257 diff --git a/Modules/AccountKit/Resources/avatars/images/animals_elephant.avatars.png b/Modules/AccountKit/Resources/avatars/images/animals_elephant.avatars.png new file mode 100755 index 0000000000..dda51c93ca --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/animals_elephant.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6fa3462dae48ed7bf7e8b8273c7a22be030384602d652af4e99fd3674000bf60 +size 13505 diff --git a/Modules/AccountKit/Resources/avatars/images/animals_fish.avatars.png b/Modules/AccountKit/Resources/avatars/images/animals_fish.avatars.png new file mode 100755 index 0000000000..69560ee399 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/animals_fish.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d868c63e4e39a26293ff66faa1b4b5af30a0c48384a628f2be0f71ad9df835d +size 12040 diff --git a/Modules/AccountKit/Resources/avatars/images/animals_fox.avatars.png b/Modules/AccountKit/Resources/avatars/images/animals_fox.avatars.png new file mode 100755 index 0000000000..c01b80f764 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/animals_fox.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9890011214689315f1cefc714e3791d4b38cf8f9958e5214a77b91920620c7f +size 12742 diff --git a/Modules/AccountKit/Resources/avatars/images/animals_giraffe.avatars.png b/Modules/AccountKit/Resources/avatars/images/animals_giraffe.avatars.png new file mode 100755 index 0000000000..7b2c5597b4 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/animals_giraffe.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f17e5f2571470431bb523af8cd4e20af3af3ff34289c1beaab1ec0e27b41de2b +size 12496 diff --git a/Modules/AccountKit/Resources/avatars/images/animals_hedgehog.avatars.png b/Modules/AccountKit/Resources/avatars/images/animals_hedgehog.avatars.png new file mode 100755 index 0000000000..686277fc81 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/animals_hedgehog.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5825c5c50c48ecfdaae5e42ea97ed80a5b51409c39f674ebec9ed52bec077678 +size 11552 diff --git a/Modules/AccountKit/Resources/avatars/images/animals_horse.avatars.png b/Modules/AccountKit/Resources/avatars/images/animals_horse.avatars.png new file mode 100755 index 0000000000..23bbbf26cd --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/animals_horse.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b2b526b723b3af90af2176f6928b7b389da1c58e3fa78bcbbf66f1069049a78 +size 12647 diff --git a/Modules/AccountKit/Resources/avatars/images/animals_koala.avatars.png b/Modules/AccountKit/Resources/avatars/images/animals_koala.avatars.png new file mode 100755 index 0000000000..fef3bdb486 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/animals_koala.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a12dfee166f14cbb14edac41e27c3ad1355cb698579f5b6865d5dd801d06038b +size 16133 diff --git a/Modules/AccountKit/Resources/avatars/images/animals_lion.avatars.png b/Modules/AccountKit/Resources/avatars/images/animals_lion.avatars.png new file mode 100755 index 0000000000..d1d2957e69 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/animals_lion.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9cacd2b2111eef2d837605e9583ead13449d5b9809167a55b7e3ac03d955a50 +size 13767 diff --git a/Modules/AccountKit/Resources/avatars/images/animals_rabbit.avatars.png b/Modules/AccountKit/Resources/avatars/images/animals_rabbit.avatars.png new file mode 100755 index 0000000000..16bf5179e7 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/animals_rabbit.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bac37c8415ad8b89cc0b09048cf864acbfb17409f78a857d9b213000d7daf805 +size 12726 diff --git a/Modules/AccountKit/Resources/avatars/images/animals_rooster.avatars.png b/Modules/AccountKit/Resources/avatars/images/animals_rooster.avatars.png new file mode 100755 index 0000000000..ddfb6f65a8 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/animals_rooster.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18d4cd1d5dc19d154b124681543cbe05694e4dde9225773879e3a5dcbf240ba0 +size 12665 diff --git a/Modules/AccountKit/Resources/avatars/images/animals_squirrel.avatars.png b/Modules/AccountKit/Resources/avatars/images/animals_squirrel.avatars.png new file mode 100755 index 0000000000..b4c8301f03 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/animals_squirrel.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:762d17b8f01403f6a04c917ca1dddaa789151d1737a3741185d4a83f14d8bb55 +size 14716 diff --git a/Modules/AccountKit/Resources/avatars/images/animals_turtle.avatars.png b/Modules/AccountKit/Resources/avatars/images/animals_turtle.avatars.png new file mode 100755 index 0000000000..32dc71b54c --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/animals_turtle.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c073d99871438b6c74c37be8448fe43d5493bcfd48654bcfdae4082d0e8cac3a +size 11734 diff --git a/Modules/AccountKit/Resources/avatars/images/anonymous.avatars.png b/Modules/AccountKit/Resources/avatars/images/anonymous.avatars.png new file mode 100755 index 0000000000..e604ae0e54 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/anonymous.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13eecad7e5115d505d1290fc4cf8bb9c78c64173317c591171f4ca69fd84e621 +size 7335 diff --git a/Modules/AccountKit/Resources/avatars/images/boy1.avatars.png b/Modules/AccountKit/Resources/avatars/images/boy1.avatars.png new file mode 100755 index 0000000000..f585376070 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/boy1.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec1a565279e33f1295f0968a79845188c90c865d5699a07c0964cb6311f7608c +size 8446 diff --git a/Modules/AccountKit/Resources/avatars/images/boy10.avatars.png b/Modules/AccountKit/Resources/avatars/images/boy10.avatars.png new file mode 100755 index 0000000000..6a263162d6 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/boy10.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:239ec8d02b539b7dc02e3b40bfa007457121b67dfefbaa0cd15aba67ff55318e +size 8878 diff --git a/Modules/AccountKit/Resources/avatars/images/boy2.avatars.png b/Modules/AccountKit/Resources/avatars/images/boy2.avatars.png new file mode 100755 index 0000000000..703c42c115 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/boy2.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9c63eab79476524f8ffecf1b4b091a4f3a0634ac8256f999d872572718efb14 +size 11687 diff --git a/Modules/AccountKit/Resources/avatars/images/boy3.avatars.png b/Modules/AccountKit/Resources/avatars/images/boy3.avatars.png new file mode 100755 index 0000000000..e2ad6bd42d --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/boy3.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:561f1eb8841a560827d657dbd87393432d01115315f58729e42e9126da648c21 +size 9222 diff --git a/Modules/AccountKit/Resources/avatars/images/boy4.avatars.png b/Modules/AccountKit/Resources/avatars/images/boy4.avatars.png new file mode 100755 index 0000000000..699d11f534 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/boy4.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a752c92d0e2bb30ec24f3bf957d6af90bdc7bc57302b6a8c2384c07fe0fff03 +size 9155 diff --git a/Modules/AccountKit/Resources/avatars/images/boy5.avatars.png b/Modules/AccountKit/Resources/avatars/images/boy5.avatars.png new file mode 100755 index 0000000000..92b05018f8 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/boy5.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f060c6e4c3961d328ea2a7ab85bbc3474547a37460bc30b82c6409baa08ab85 +size 8958 diff --git a/Modules/AccountKit/Resources/avatars/images/boy6.avatars.png b/Modules/AccountKit/Resources/avatars/images/boy6.avatars.png new file mode 100755 index 0000000000..5c2f44ba85 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/boy6.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03781beb871931245ec5b0969e71245a10c7a291f40c654d28b3dd8f8413d92b +size 8850 diff --git a/Modules/AccountKit/Resources/avatars/images/boy7.avatars.png b/Modules/AccountKit/Resources/avatars/images/boy7.avatars.png new file mode 100755 index 0000000000..9f5490b53f --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/boy7.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26b81d0c559f2f5116caae8690f1d0749d76c418c4e9239ae7aacd71c5b1b1d6 +size 8688 diff --git a/Modules/AccountKit/Resources/avatars/images/boy8.avatars.png b/Modules/AccountKit/Resources/avatars/images/boy8.avatars.png new file mode 100755 index 0000000000..69b862ea6f --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/boy8.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:043b177605dd21b8c0aecc756a4b8eca458ad51aac83cb1457f84b116f2f5043 +size 6898 diff --git a/Modules/AccountKit/Resources/avatars/images/boy9.avatars.png b/Modules/AccountKit/Resources/avatars/images/boy9.avatars.png new file mode 100755 index 0000000000..5329f51923 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/boy9.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a86f0eea91be62b878461eae7d5a3b9fe0eccba482c3b4b23610cdcc6ab2a15 +size 11800 diff --git a/Modules/AccountKit/Resources/avatars/images/eyeglasses_blue_leka.avatars.png b/Modules/AccountKit/Resources/avatars/images/eyeglasses_blue_leka.avatars.png new file mode 100755 index 0000000000..197deb92ec --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/eyeglasses_blue_leka.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ab2616c7784be7047b64331a5dac47b61e3bb7f35b93da72293dd0ad65f3d99 +size 21607 diff --git a/Modules/AccountKit/Resources/avatars/images/eyeglasses_green_leka.avatars.png b/Modules/AccountKit/Resources/avatars/images/eyeglasses_green_leka.avatars.png new file mode 100755 index 0000000000..51c6b62e73 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/eyeglasses_green_leka.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7fdb114d859d75531f8e8842058ecbb058e4ce7451dd031351246e6efdcbbf61 +size 21682 diff --git a/Modules/AccountKit/Resources/avatars/images/eyeglasses_pink_leka.avatars.png b/Modules/AccountKit/Resources/avatars/images/eyeglasses_pink_leka.avatars.png new file mode 100755 index 0000000000..472b3675ee --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/eyeglasses_pink_leka.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18c2a799661cc684668bb09cab810f64f4fcb622fbb36949a8688bad295dfab1 +size 21508 diff --git a/Modules/AccountKit/Resources/avatars/images/eyeglasses_yellow_leka.avatars.png b/Modules/AccountKit/Resources/avatars/images/eyeglasses_yellow_leka.avatars.png new file mode 100755 index 0000000000..fc79a90a73 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/eyeglasses_yellow_leka.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c123dbd205e12f83893ff0cd93f003ee12bc3ce522bfb858dea3210c25f5a30c +size 20601 diff --git a/Modules/AccountKit/Resources/avatars/images/fruits_apple_green.avatars.png b/Modules/AccountKit/Resources/avatars/images/fruits_apple_green.avatars.png new file mode 100755 index 0000000000..ea1dde4f01 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/fruits_apple_green.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e65351423c631e117768e3b66c39317a8188137526141ad64381d3b2726d2217 +size 7216 diff --git a/Modules/AccountKit/Resources/avatars/images/fruits_apple_red.avatars.png b/Modules/AccountKit/Resources/avatars/images/fruits_apple_red.avatars.png new file mode 100755 index 0000000000..0b8bd63bfb --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/fruits_apple_red.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e197fedbf17dcbabdecdeb9d111d17bfcc77ed147f83754f70da3cacfb207c01 +size 7076 diff --git a/Modules/AccountKit/Resources/avatars/images/fruits_banana.avatars.png b/Modules/AccountKit/Resources/avatars/images/fruits_banana.avatars.png new file mode 100755 index 0000000000..cf11f3ebde --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/fruits_banana.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96fe6371dfeea996e907931ca9f22bf83839be8d8dd6738f1172162e572ef14b +size 9968 diff --git a/Modules/AccountKit/Resources/avatars/images/fruits_cherry.avatars.png b/Modules/AccountKit/Resources/avatars/images/fruits_cherry.avatars.png new file mode 100755 index 0000000000..c79602cc7e --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/fruits_cherry.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5dd72c994f78b9ecf6090ab76c3b1d6a811e0450c88f67f58ffe330925910369 +size 10027 diff --git a/Modules/AccountKit/Resources/avatars/images/fruits_grape.avatars.png b/Modules/AccountKit/Resources/avatars/images/fruits_grape.avatars.png new file mode 100755 index 0000000000..843d79e387 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/fruits_grape.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a35f8e52ddbed992eba4c6a25009177d89fbe98f5208977240a140b94cd5b3cb +size 15390 diff --git a/Modules/AccountKit/Resources/avatars/images/fruits_kiwi.avatars.png b/Modules/AccountKit/Resources/avatars/images/fruits_kiwi.avatars.png new file mode 100755 index 0000000000..e49de4bc13 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/fruits_kiwi.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb21f7dd2ae35940314093cdf6756c9f2832a8cc9e0e9f123f3d6f57bfa9ad7c +size 15908 diff --git a/Modules/AccountKit/Resources/avatars/images/fruits_lemon.avatars.png b/Modules/AccountKit/Resources/avatars/images/fruits_lemon.avatars.png new file mode 100755 index 0000000000..ab69fdfad4 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/fruits_lemon.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa0d245777d5a16ce70f8f19ebad3070f6733495740f4c6e022cf7bf5c5fc188 +size 8265 diff --git a/Modules/AccountKit/Resources/avatars/images/fruits_pear.avatars.png b/Modules/AccountKit/Resources/avatars/images/fruits_pear.avatars.png new file mode 100755 index 0000000000..85c0824815 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/fruits_pear.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52e20d704f7761f620a6abcfb2fe287c9d42557fcd091384fe579b39028d002b +size 9087 diff --git a/Modules/AccountKit/Resources/avatars/images/fruits_pineapple.avatars.png b/Modules/AccountKit/Resources/avatars/images/fruits_pineapple.avatars.png new file mode 100755 index 0000000000..c1e09fe26c --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/fruits_pineapple.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31f793b25374746db013441672476d6d1747f5def69fc82ab9b8d3e391820d06 +size 14125 diff --git a/Modules/AccountKit/Resources/avatars/images/fruits_strawberry.avatars.png b/Modules/AccountKit/Resources/avatars/images/fruits_strawberry.avatars.png new file mode 100755 index 0000000000..59d95fd232 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/fruits_strawberry.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4099c24b4e63d45fe983403f09b5e9d5dfbad82c7172d289b03990c74ce39bc +size 10729 diff --git a/Modules/AccountKit/Resources/avatars/images/fruits_watermelon.avatars.png b/Modules/AccountKit/Resources/avatars/images/fruits_watermelon.avatars.png new file mode 100755 index 0000000000..b54592a2c0 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/fruits_watermelon.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9dbf32963a919c487ebb38ec15028b850dd8202b2ad9b281d99a4efa0602d05 +size 7123 diff --git a/Modules/AccountKit/Resources/avatars/images/girl1.avatars.png b/Modules/AccountKit/Resources/avatars/images/girl1.avatars.png new file mode 100755 index 0000000000..ce819c5e79 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/girl1.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77aeb66cac2b28465ba6f1ad92a601ca36f0d46ef5a8c47aeff1f5d4b26ad2d3 +size 10158 diff --git a/Modules/AccountKit/Resources/avatars/images/girl10.avatars.png b/Modules/AccountKit/Resources/avatars/images/girl10.avatars.png new file mode 100755 index 0000000000..535aa83fc4 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/girl10.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:283d61297b2e9989a5fe6baa57f5de5f0a6b79bda5c8d25dd4019bfc83c11930 +size 11769 diff --git a/Modules/AccountKit/Resources/avatars/images/girl2.avatars.png b/Modules/AccountKit/Resources/avatars/images/girl2.avatars.png new file mode 100755 index 0000000000..b0955b3d81 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/girl2.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9401b77b812da795137e287c4499435c5f512d22a8c94197c3a9190df82abdaf +size 11053 diff --git a/Modules/AccountKit/Resources/avatars/images/girl3.avatars.png b/Modules/AccountKit/Resources/avatars/images/girl3.avatars.png new file mode 100755 index 0000000000..257232f315 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/girl3.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a14bc158e3eb54a82a6c11190b1ebd6deb3da223969322ae9482676ead5cf9e9 +size 9421 diff --git a/Modules/AccountKit/Resources/avatars/images/girl4.avatars.png b/Modules/AccountKit/Resources/avatars/images/girl4.avatars.png new file mode 100755 index 0000000000..db4485cf7b --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/girl4.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dda74dfeba5cc4d458243361d0dc75db137e4eea0c3e26309493484a6127e1ca +size 10472 diff --git a/Modules/AccountKit/Resources/avatars/images/girl5.avatars.png b/Modules/AccountKit/Resources/avatars/images/girl5.avatars.png new file mode 100755 index 0000000000..71eea787e7 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/girl5.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ccb7d9f31d4deb1fd7f1f2d52f651a718f0c52a6e1dbc85b597c57408c030fcb +size 11030 diff --git a/Modules/AccountKit/Resources/avatars/images/girl6.avatars.png b/Modules/AccountKit/Resources/avatars/images/girl6.avatars.png new file mode 100755 index 0000000000..c2ca36632d --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/girl6.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51fefd764af91b16b5377308f46a65744263f68b467037df57721966349d5fa1 +size 10753 diff --git a/Modules/AccountKit/Resources/avatars/images/girl7.avatars.png b/Modules/AccountKit/Resources/avatars/images/girl7.avatars.png new file mode 100755 index 0000000000..5d73a77773 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/girl7.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:615c87dab7367068ed72f7c4e386af0f5a90dad18a657375f9bd975c8d990e31 +size 11021 diff --git a/Modules/AccountKit/Resources/avatars/images/girl8.avatars.png b/Modules/AccountKit/Resources/avatars/images/girl8.avatars.png new file mode 100755 index 0000000000..260c3fac17 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/girl8.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8be4b17b9bcf42ffac02627732518341b1efe7e5b9760c19b679683d2a91119d +size 11224 diff --git a/Modules/AccountKit/Resources/avatars/images/girl9.avatars.png b/Modules/AccountKit/Resources/avatars/images/girl9.avatars.png new file mode 100755 index 0000000000..600bf7e834 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/girl9.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66c03c8b9d1d22eea72dd3c4ea14909b442ff0974c013a120668ca29ae2c31e0 +size 8930 diff --git a/Modules/AccountKit/Resources/avatars/images/professions_astronaut_leka.avatars.png b/Modules/AccountKit/Resources/avatars/images/professions_astronaut_leka.avatars.png new file mode 100755 index 0000000000..b54c3ee67e --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/professions_astronaut_leka.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4495ab8844f23eeefa04b657bcfb05fccabd98bbc59d3a4ba8ad508f5cacf0f +size 19394 diff --git a/Modules/AccountKit/Resources/avatars/images/professions_cook_leka.avatars.png b/Modules/AccountKit/Resources/avatars/images/professions_cook_leka.avatars.png new file mode 100755 index 0000000000..3fd30598d8 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/professions_cook_leka.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:910b6e733c00c60aed639184b20e4d138f8978996a7f22b36d752ad4031449e7 +size 14089 diff --git a/Modules/AccountKit/Resources/avatars/images/professions_doctor_leka.avatars.png b/Modules/AccountKit/Resources/avatars/images/professions_doctor_leka.avatars.png new file mode 100755 index 0000000000..ff5696aeaf --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/professions_doctor_leka.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d2e7ea822420e6ed6afabd1b0b7a23e65e2dfdeac2ee2bced12be28b14004f8 +size 12775 diff --git a/Modules/AccountKit/Resources/avatars/images/professions_explorer_leka.avatars.png b/Modules/AccountKit/Resources/avatars/images/professions_explorer_leka.avatars.png new file mode 100755 index 0000000000..24eb4be995 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/professions_explorer_leka.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71e500ec7c2bca357bfef0434f48ee7ca9a298d1303970d68a7b1ac3ad1d0f27 +size 11536 diff --git a/Modules/AccountKit/Resources/avatars/images/professions_pirate_leka.avatars.png b/Modules/AccountKit/Resources/avatars/images/professions_pirate_leka.avatars.png new file mode 100755 index 0000000000..376481ca4e --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/professions_pirate_leka.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40a85e69878627d9a8d222a6f5e79ec799357411501798908d99f907847b5522 +size 14483 diff --git a/Modules/AccountKit/Resources/avatars/images/professions_sailor_leka.avatars.png b/Modules/AccountKit/Resources/avatars/images/professions_sailor_leka.avatars.png new file mode 100755 index 0000000000..cea0bbc828 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/professions_sailor_leka.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eedb989f6c88c4414bf237b3b9ed360efadfad92c66cd32187f7063a1b090851 +size 16923 diff --git a/Modules/AccountKit/Resources/avatars/images/vegetables_avocado.avatars.png b/Modules/AccountKit/Resources/avatars/images/vegetables_avocado.avatars.png new file mode 100755 index 0000000000..2e316103a3 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/vegetables_avocado.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:155d440ddddd193c98c58e0a40877312bfcc78545f55be2b5f204a698cb00116 +size 12907 diff --git a/Modules/AccountKit/Resources/avatars/images/vegetables_broccoli.avatars.png b/Modules/AccountKit/Resources/avatars/images/vegetables_broccoli.avatars.png new file mode 100755 index 0000000000..83b69aab8f --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/vegetables_broccoli.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33b2693f21bcdea131e58cf1ea4b190987d80f9d5660cd24bbf8d6aed2530283 +size 12340 diff --git a/Modules/AccountKit/Resources/avatars/images/vegetables_carrot.avatars.png b/Modules/AccountKit/Resources/avatars/images/vegetables_carrot.avatars.png new file mode 100755 index 0000000000..8079f7d298 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/vegetables_carrot.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b495620bcec634b451aa37bb9d347abfce96e4816203a7416fce0242604ddb73 +size 6989 diff --git a/Modules/AccountKit/Resources/avatars/images/vegetables_corn.avatars.png b/Modules/AccountKit/Resources/avatars/images/vegetables_corn.avatars.png new file mode 100755 index 0000000000..fb9a947eec --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/vegetables_corn.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48c5775804369ccb367bd01dc07776ccb6adfa363ffd9cc868482462469f3283 +size 13673 diff --git a/Modules/AccountKit/Resources/avatars/images/vegetables_eggplant.avatars.png b/Modules/AccountKit/Resources/avatars/images/vegetables_eggplant.avatars.png new file mode 100755 index 0000000000..25425bc0d2 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/vegetables_eggplant.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a9034d009a156011fdace54a1f20607db3a283f947b5153882eb79a59254bd8 +size 8841 diff --git a/Modules/AccountKit/Resources/avatars/images/vegetables_onion.avatars.png b/Modules/AccountKit/Resources/avatars/images/vegetables_onion.avatars.png new file mode 100755 index 0000000000..a30b398207 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/vegetables_onion.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8543a93b2494a6b75ee16260e036087ca3f12f94a17913479d0939a95fee309c +size 11305 diff --git a/Modules/AccountKit/Resources/avatars/images/vegetables_potato.avatars.png b/Modules/AccountKit/Resources/avatars/images/vegetables_potato.avatars.png new file mode 100755 index 0000000000..f6ab2d961a --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/vegetables_potato.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43e82cc6e50ff0ecdbefb1c52a1fbd162229468bed7ec8eb9b275f2311801ca9 +size 8735 diff --git a/Modules/AccountKit/Resources/avatars/images/vegetables_salad.avatars.png b/Modules/AccountKit/Resources/avatars/images/vegetables_salad.avatars.png new file mode 100755 index 0000000000..1c22db4b16 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/vegetables_salad.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e83ce9338932550f93514a0bc8cc5bea0c8332f012cc9fc3d4fb4dfb7b7a3be4 +size 16941 diff --git a/Modules/AccountKit/Resources/avatars/images/vegetables_tomato.avatars.png b/Modules/AccountKit/Resources/avatars/images/vegetables_tomato.avatars.png new file mode 100755 index 0000000000..ee4ee90187 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/vegetables_tomato.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cda26da4cd8a4b1e330cc0eda1ee89dc58742d0a9e4b7527da2d866842a7abdf +size 11541 diff --git a/Modules/AccountKit/Resources/avatars/images/weather_cloud_with_face.avatars.png b/Modules/AccountKit/Resources/avatars/images/weather_cloud_with_face.avatars.png new file mode 100755 index 0000000000..4f3c32327e --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/weather_cloud_with_face.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:019a474e4b05b6384ae9c1446a0c6fcf477efc49cac799aa63af5519bf5c3821 +size 8687 diff --git a/Modules/AccountKit/Resources/avatars/images/weather_moon_with_face.avatars.png b/Modules/AccountKit/Resources/avatars/images/weather_moon_with_face.avatars.png new file mode 100755 index 0000000000..44623ffed3 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/weather_moon_with_face.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bed1df680017be273fe45d555e6a6d447caed011a5dc0ec8004c60bc5a855cfe +size 14729 diff --git a/Modules/AccountKit/Resources/avatars/images/weather_snowflake_with_face.avatars.png b/Modules/AccountKit/Resources/avatars/images/weather_snowflake_with_face.avatars.png new file mode 100755 index 0000000000..71ef076531 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/weather_snowflake_with_face.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3242b560d664b61230bfd987a68da8ea153452782ddd321231d2d80e0a06a1be +size 20395 diff --git a/Modules/AccountKit/Resources/avatars/images/weather_star_with_face.avatars.png b/Modules/AccountKit/Resources/avatars/images/weather_star_with_face.avatars.png new file mode 100755 index 0000000000..c2671266ec --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/weather_star_with_face.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e21a0693fb98e94cb2ca4be9ede963963930988c8c55327ad24095b157ae4164 +size 10594 diff --git a/Modules/AccountKit/Resources/avatars/images/weather_sun_with_face.avatars.png b/Modules/AccountKit/Resources/avatars/images/weather_sun_with_face.avatars.png new file mode 100755 index 0000000000..efc6f3c4f1 --- /dev/null +++ b/Modules/AccountKit/Resources/avatars/images/weather_sun_with_face.avatars.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b35bafb2af4b340fc312688c101149d65727915c71e433587282d31e4a7e7d4 +size 15091 diff --git a/Modules/AccountKit/Resources/data/professions.yml b/Modules/AccountKit/Resources/data/professions.yml new file mode 100644 index 0000000000..aa9ab07fce --- /dev/null +++ b/Modules/AccountKit/Resources/data/professions.yml @@ -0,0 +1,455 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 +list: + - id: aba_therapist + l10n: + - locale: fr_FR + name: Intervenant(e) en ABA + description: | + Spécialiste appliquant les principes de l'analyse appliquée du comportement (ABA) pour évaluer et intervenir auprès de personnes ayant des troubles du spectre autistique et autres troubles comportementaux. + - locale: en_US + name: ABA Therapist + description: | + A specialist who applies the principles of Applied Behavior Analysis (ABA) to assess and intervene in individuals with autism spectrum disorders and other behavioral issues. + + - id: behavior_analyst + l10n: + - locale: fr_FR + name: Analyste du comportement + description: | + Professionnel spécialisé dans l'étude du comportement humain, utilisant des techniques d'analyse pour comprendre et modifier les comportements problématiques. + - locale: en_US + name: Behavior Analyst + description: | + A professional specialized in the study of human behavior, using analysis techniques to understand and modify problematic behaviors. + + - id: behavioral_therapist + l10n: + - locale: fr_FR + name: Thérapeute Comportemental + description: | + Le thérapeute comportemental aide les individus présentant des défis comportementaux, en utilisant des techniques pour améliorer la communication, les compétences sociales et le comportement. + - locale: en_US + name: Behavioral Therapist + description: | + The Behavioral Therapist assists individuals with behavioral challenges, employing techniques to improve communication, social skills, and behavior. + + - id: care_assistant + l10n: + - locale: fr_FR + name: Auxiliaire de vie + description: | + L'Auxiliaire de vie offre un soutien aux personnes ayant besoin de soins, en assistant avec les tâches quotidiennes et en contribuant à leur confort et à leur santé. + - locale: en_US + name: Care Assistant + description: | + The Care Assistant provides support to individuals in need of care, assisting with daily tasks and contributing to their comfort and health. + + - id: caregiver + l10n: + - locale: fr_FR + name: Aide-soignant(e) + description: | + L'aide-soignant assiste les personnes dépendantes dans les activités de la vie quotidienne, jouant un rôle essentiel dans le maintien de leur bien-être. + - locale: en_US + name: Caregiver + description: | + The caregiver assists dependent individuals in daily life activities, playing an essential role in maintaining their well-being. + + - id: child_psychiatrist + l10n: + - locale: fr_FR + name: Pédopsychiatre + description: | + Le pédopsychiatre diagnostique et traite les troubles mentaux et émotionnels chez les enfants et adolescents. + - locale: en_US + name: Child Psychiatrist + description: | + The child psychiatrist diagnoses and treats mental and emotional disorders in children and adolescents. + + - id: clinical_director + l10n: + - locale: fr_FR + name: Directeur(trice) clinique + description: | + Responsable de la supervision clinique et administrative d'une unité de soins ou d'un établissement de santé, garantissant la qualité des soins et le respect des normes professionnelles. + - locale: en_US + name: Clinical Director + description: | + Responsible for the clinical and administrative supervision of a care unit or health facility, ensuring the quality of care and compliance with professional standards. + + - id: coordinator + l10n: + - locale: fr_FR + name: Coordinateur + description: | + Le Coordinateur assure la liaison entre différentes équipes et services, en facilitant la communication et la collaboration pour atteindre les objectifs communs et optimiser les processus. + - locale: en_US + name: Coordinator + description: | + The Coordinator acts as a liaison between different teams and services, facilitating communication and collaboration to achieve common goals and optimize processes. + + - id: deputy_director + l10n: + - locale: fr_FR + name: Directeur Adjoint + description: | + Le directeur adjoint assiste le directeur dans la gestion et la direction, prenant en charge des responsabilités spécifiques et contribuant à la mise en œuvre de la stratégie globale. + - locale: en_US + name: Deputy Director + description: | + The deputy director assists the director in management and leadership, taking on specific responsibilities and contributing to the implementation of the overall strategy. + + - id: director + l10n: + - locale: fr_FR + name: Directeur + description: | + Le directeur gère et dirige une institution ou un service, en élaborant des stratégies, en supervisant les opérations et en assurant le leadership et la direction de l'équipe. + - locale: en_US + name: Director + description: | + The director manages and leads an institution or service, developing strategies, overseeing operations, and providing leadership and direction to the team. + + - id: doctor + l10n: + - locale: fr_FR + name: Médecin + description: | + Le médecin diagnostique et traite les maladies, jouant un rôle clé dans le suivi médical des personnes en situation de handicap. + - locale: en_US + name: Doctor + description: | + The doctor diagnoses and treats illnesses, playing a key role in the medical follow-up of individuals with disabilities. + + - id: early_childhood_educator + l10n: + - locale: fr_FR + name: Éducateur(trice) de jeunes enfants + description: | + Ce professionnel accompagne le développement des jeunes enfants en créant un environnement adapté et stimulant. + - locale: en_US + name: Early Childhood Educator + description: | + This professional supports the development of young children by creating a nurturing and stimulating environment. + + - id: educational_and_social_assistant + l10n: + - locale: fr_FR + name: Accompagnant Éducatif et Social + description: | + L'accompagnant éducatif et Social aide les personnes en difficulté ou en situation de vulnérabilité, en leur fournissant un soutien dans leur éducation et leur intégration sociale. + - locale: en_US + name: Educational and Social Assistant + description: | + The educational and social assistant supports individuals facing difficulties or vulnerabilities, providing them with assistance in their education and social integration. + + - id: educational_and_social_support_worker + l10n: + - locale: fr_FR + name: Travailleur social éducatif + description: | + Le travailleur social éducatif aide les personnes en difficulté sociale à surmonter leurs obstacles et à s'intégrer dans la société. + - locale: en_US + name: Educational and Social Support Worker + description: | + The educational social worker helps individuals facing social difficulties to overcome their challenges and integrate into society. + + - id: educational_monitor + l10n: + - locale: fr_FR + name: Moniteur(trice) éducatif(ve) + description: | + Le moniteur éducatif encadre des activités éducatives et de loisirs pour différents publics, souvent dans un cadre institutionnel. + - locale: en_US + name: Educational Monitor + description: | + The educational monitor supervises educational and recreational activities for various groups, often within an institutional setting. + + - id: educational_support_for_students_with_disabilities + l10n: + - locale: fr_FR + name: Auxiliaire de vie scolaire pour élèves en situation de handicap + description: | + Ce professionnel accompagne les élèves en situation de handicap dans leur parcours scolaire, facilitant leur accès à l'éducation et à l'apprentissage. + - locale: en_US + name: Educational Support for Students with Disabilities + description: | + This professional accompanies students with disabilities in their educational journey, facilitating their access to education and learning. + + - id: health_manager + l10n: + - locale: fr_FR + name: Cadre de santé + description: | + Le cadre de santé supervise les aspects cliniques et administratifs dans les établissements de santé, en assurant la coordination des soins et la gestion des ressources humaines et matérielles. + - locale: en_US + name: Health Manager + description: | + The health manager oversees clinical and administrative aspects in healthcare facilities, ensuring care coordination and the management of human and material resources. + + - id: inclusive_education_specialist + l10n: + - locale: fr_FR + name: Spécialiste de l'Éducation Inclusive + description: | + Le spécialiste de l'éducation inclusive est expert dans l'adaptation des programmes d'études et des stratégies d'enseignement pour inclure des élèves de toutes capacités. + - locale: en_US + name: Inclusive Education Specialist + description: | + The inclusive education specialist is an expert in adapting curricula and teaching strategies to include students of all abilities. + + - id: medical_psychological_assistant + l10n: + - locale: fr_FR + name: Aide Médico Psychologique + description: | + L'aide médico psychologique (AMP) accompagne les personnes ayant des troubles psychologiques ou des handicaps, en fournissant un soutien quotidien et en contribuant à leur bien-être psychologique. + - locale: en_US + name: Medical Psychological Assistant + description: | + The medical psychological assistant supports individuals with psychological disorders or disabilities, providing daily support and contributing to their psychological well-being. + + - id: nurse + l10n: + - locale: fr_FR + name: Infirmier(ère) + description: | + L'infirmier assure les soins médicaux quotidiens et soutient les patients dans la gestion de leur santé au quotidien. + - locale: en_US + name: Nurse + description: | + The nurse provides daily medical care and supports patients in managing their health on a day-to-day basis. + + - id: nursery_assistant + l10n: + - locale: fr_FR + name: Auxiliaire de Puériculture + description: | + L'auxiliaire de puériculture travaille auprès des jeunes enfants, assurant leur soin quotidien, leur éveil et contribuant à leur bien-être dans les structures d'accueil de la petite enfance. + - locale: en_US + name: Nursery Assistant + description: | + The nursery assistant works with young children, providing their daily care, stimulating their development, and contributing to their well-being in early childhood care settings. + + - id: nursery_nurse + l10n: + - locale: fr_FR + name: Infirmier(ère) de Puériculture + description: | + L'infirmier(ère) de puériculture se spécialise dans les soins aux nourrissons et aux jeunes enfants, contribuant à leur développement et à leur santé dans un cadre de soins infirmiers. + - locale: en_US + name: Nursery Nurse + description: | + A nursery nurse specializes in caring for infants and young children, contributing to their development and health within a nursing care framework. + + - id: occupational_therapist + l10n: + - locale: fr_FR + name: Ergothérapeute + description: | + L'ergothérapeute aide les personnes handicapées à retrouver autonomie et confort dans les activités quotidiennes, en adaptant l'environnement et les outils. + - locale: en_US + name: Occupational Therapist + description: | + The occupational therapist assists disabled individuals in regaining autonomy and comfort in daily activities, adapting the environment and tools. + + - id: personal_care_assistant_for_dependence_and_disability + l10n: + - locale: fr_FR + name: Assistant(e) de vie aux familles pour dépendance et handicap + description: | + Cet assistant soutient les familles dans la prise en charge de membres dépendants ou handicapés, facilitant la vie quotidienne. + - locale: en_US + name: Personal Care Assistant for Dependence and Disability + description: | + This assistant supports families in caring for dependent or disabled members, facilitating daily life. + + - id: physiotherapist + l10n: + - locale: fr_FR + name: Kinésithérapeute + description: | + Le kinésithérapeute pratique des thérapies manuelles et exercices pour rétablir la mobilité et atténuer les douleurs. + - locale: en_US + name: Physiotherapist + description: | + The physiotherapist performs manual therapies and exercises to restore mobility and alleviate pain. + + - id: psychiatrist + l10n: + - locale: fr_FR + name: Psychiatre + description: | + Le psychiatre diagnostique et traite les troubles mentaux. Ils travaillent souvent avec des patients ayant des besoins spéciaux, en fournissant une évaluation médicale, des soins et des plans de traitement personnalisés. + - locale: en_US + name: Psychiatrist + description: | + The psychiatrist diagnoses and treats mental disorders. They often work with patients with special needs, providing medical assessment, care, and personalized treatment plans. + + - id: psychologist + l10n: + - locale: fr_FR + name: Psychologue + description: | + Le psychologue évalue et soutient la santé mentale, apportant une aide spécialisée dans les contextes éducatifs et thérapeutiques. + - locale: en_US + name: Psychologist + description: | + The psychologist assesses and supports mental health, providing specialized assistance in educational and therapeutic contexts. + + - id: psychomotor_therapist + l10n: + - locale: fr_FR + name: Psychomotricien(ne) + description: | + Le psychomotricien intervient auprès de personnes présentant des troubles psychomoteurs, en utilisant diverses techniques thérapeutiques. + - locale: en_US + name: Psychomotor Therapist + description: | + The psychomotor therapist works with individuals having psychomotor disorders, using various therapeutic techniques. + + - id: psychotherapist + l10n: + - locale: fr_FR + name: Psychologue comportementaliste + description: | + Psychologue spécialisé dans l'utilisation de thérapies comportementales et cognitives pour traiter divers troubles psychologiques et comportementaux. + - locale: en_US + name: Psychotherapist + description: | + A psychologist specializing in the use of behavioral and cognitive therapies to treat various psychological and behavioral disorders. + - id: rehabilitation_counselor + l10n: + - locale: fr_FR + name: Conseiller en Réadaptation + description: | + Le conseiller en réadaptation travaille avec des personnes handicapées pour atteindre leurs objectifs personnels, professionnels et de vie autonome. + - locale: en_US + name: Rehabilitation Counselor + description: | + The rehabilitation counselor works with individuals with disabilities to achieve their personal, career, and independent living goals. + + - id: remedial_education_therapist + l10n: + - locale: fr_FR + name: Orthopédagogue + description: | + L'orthopédagogue conçoit et met en œuvre des programmes de soutien éducatif pour les personnes ayant des difficultés d'apprentissage, visant à améliorer leurs compétences et performances académiques. + - locale: en_US + name: Remedial Education Therapist + description: | + The remedial education therapist designs and implements educational support programs for individuals with learning difficulties, aiming to enhance their skills and academic performance. + + - id: service_manager + l10n: + - locale: fr_FR + name: Chef de service + description: | + Le chef de service est responsable de la gestion quotidienne d'un service spécifique, en veillant à la qualité des services fournis et au bien-être de l'équipe. + - locale: en_US + name: Service Manager + description: | + The service manager is responsible for the daily management of a specific service, ensuring the quality of services provided and the well-being of the team. + + - id: social_life_assistant + l10n: + - locale: fr_FR + name: Assistant(e) de Vie Sociale + description: | + L'assistant(e) de vie sociale soutient les personnes en situation de fragilité ou de dépendance dans les activités du quotidien, en favorisant leur bien-être et leur intégration sociale. + - locale: en_US + name: Social Life Assistant + description: | + The social life assistant supports individuals in situations of fragility or dependency with daily activities, promoting their well-being and social integration. + + - id: socio_cultural_animator + l10n: + - locale: fr_FR + name: Animateur(trice) socio-culturel(le) + description: | + L'animateur socio-culturel organise des activités de loisir et d'animation qui favorisent le lien social et l'épanouissement individuel. + - locale: en_US + name: Socio-cultural Animator + description: | + The socio-cultural animator organizes leisure and entertainment activities that promote social bonding and individual fulfillment. + + - id: special_education_teacher + l10n: + - locale: fr_FR + name: Enseignant(e) Spécialisé(e) + description: | + L'enseignant en éducation spécialisée travaille spécifiquement avec des enfants présentant un large éventail de handicaps, en adaptant les méthodes d'enseignement et le contenu pédagogique pour répondre à leurs besoins uniques. + - locale: en_US + name: Special Education Teacher + description: | + The special education teacher specifically works with children who have a wide range of disabilities, adapting teaching methods and educational content to meet their unique needs. + + - id: special_educator + l10n: + - locale: fr_FR + name: Éducateur(trice) spécialisé(e) + description: | + L'éducateur spécialisé intervient auprès de personnes en difficulté, handicapées ou en situation de risque d'exclusion, pour favoriser leur intégration sociale. + - locale: en_US + name: Specialized Educator + description: | + Specialized educators work with individuals facing difficulties, disabilities, or risk of exclusion, to promote their social integration. + + - id: speech_language_therapist + l10n: + - locale: fr_FR + name: Orthophoniste + description: | + L'orthophoniste traite les troubles de la parole et du langage, élaborant des programmes de rééducation personnalisés. + - locale: en_US + name: Speech-Language Therapist + description: | + The speech therapist treats speech and language disorders, developing personalized rehabilitation programs. + + - id: sports_trainer + l10n: + - locale: fr_FR + name: Éducateur(trice) sportif(tive) + description: | + L'éducateur sportif conçoit et supervise des programmes d'entraînement pour améliorer la condition physique, souvent adaptés aux besoins spécifiques des personnes handicapées ou en rééducation. + - locale: en_US + name: Sports Trainer + description: | + The sports trainer designs and oversees training programs to improve physical fitness, often tailored to the specific needs of individuals with disabilities or in rehabilitation. + + - id: teacher + l10n: + - locale: fr_FR + name: Enseignant(e) + description: | + L'enseignant transmet des connaissances et compétences selon les programmes éducatifs, adaptant souvent son approche pour les élèves à besoins spéciaux. + - locale: en_US + name: Teacher + description: | + A teacher imparts knowledge and skills according to educational programs, often adapting their approach for students with special needs. + + - id: toy_librarian + l10n: + - locale: fr_FR + name: Ludothécaire + description: | + Le ludothécaire gère une ludothèque, proposant une variété de jeux et jouets, et favorise l'éveil et l'apprentissage des enfants par le jeu, tout en accompagnant les familles et éducateurs. + - locale: en_US + name: Toy Librarian + description: | + The toy librarian manages a toy library, offering a variety of games and toys, and promotes children's development and learning through play, while supporting families and educators. + + - id: workshop_monitor + l10n: + - locale: fr_FR + name: Moniteur(trice) d'atelier + description: | + Ce professionnel organise et anime des ateliers à visée thérapeutique ou éducative pour des personnes en situation de handicap ou de réinsertion. + - locale: en_US + name: Workshop Monitor + description: | + This professional organizes and leads workshops with therapeutic or educational aims for individuals with disabilities or in reintegration. diff --git a/Modules/AccountKit/Sources/AccountKit.swift b/Modules/AccountKit/Sources/AccountKit.swift new file mode 100644 index 0000000000..b9ab3f0532 --- /dev/null +++ b/Modules/AccountKit/Sources/AccountKit.swift @@ -0,0 +1,7 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LogKit + +let log = LogKit.createLoggerFor(module: "AccountKit") diff --git a/Modules/AccountKit/Sources/Authentication/AuthManager+l10n.swift b/Modules/AccountKit/Sources/Authentication/AuthManager+l10n.swift new file mode 100644 index 0000000000..f9036cd8d9 --- /dev/null +++ b/Modules/AccountKit/Sources/Authentication/AuthManager+l10n.swift @@ -0,0 +1,29 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +extension l10n { + enum AuthManager { + static let signupFailedError = LocalizedString("accountkit.auth_manager.signup_failed_error", + bundle: AccountKitResources.bundle, + value: "Sign-up failed. Please try again later.", + comment: "Sign-up failure error message") + + static let signInFailedError = LocalizedString("accountkit.auth_manager.signin_failed_error", + bundle: AccountKitResources.bundle, + value: "Sign-in failed. Please try again.", + comment: "Sign-in failure error message") + + static let verificationEmailFailure = LocalizedString("accountkit.auth_manager.verification_email_failed_error", + bundle: AccountKitResources.bundle, + value: "There was an error sending the verification email. Please try again later.", + comment: "Verification email failure error message") + + static let signOutFailedError = LocalizedString("accountkit.auth_manager.signout_failed_error", + bundle: AccountKitResources.bundle, + value: "Failed to sign out. Please try again.", + comment: "Sign-out failure error message") + } +} diff --git a/Modules/AccountKit/Sources/Authentication/AuthManager.swift b/Modules/AccountKit/Sources/Authentication/AuthManager.swift new file mode 100644 index 0000000000..0cb4aeabe4 --- /dev/null +++ b/Modules/AccountKit/Sources/Authentication/AuthManager.swift @@ -0,0 +1,136 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import Firebase +import FirebaseAuthCombineSwift +import Foundation +import LocalizationKit + +public class AuthManager { + // MARK: Lifecycle + + private init() { + self.auth.addStateDidChangeListener { [weak self] _, user in + self?.updateAuthState(for: user) + } + } + + // MARK: Public + + public enum AuthenticationState { + case unknown + case loggedOut + case loggedIn + } + + public enum UserAction { + case userIsSigningUp + case userIsSigningIn + } + + public static let shared = AuthManager() + + public var currentUserEmail: String? { + self.auth.currentUser?.email + } + + public var authenticationStatePublisher: AnyPublisher { + self.authenticationState.eraseToAnyPublisher() + } + + public func signUp(email: String, password: String) { + self.loadingStatePublisher.send(true) + self.auth.createUser(withEmail: email, password: password) + .mapError { $0 as Error } + .sink(receiveCompletion: { [weak self] completion in + self?.loadingStatePublisher.send(false) + switch completion { + case .finished: + break + case let .failure(error): + log.error("\(error.localizedDescription)") + let errorMessage = String(l10n.AuthManager.signupFailedError.characters) + self?.authenticationError.send(AuthenticationError.custom(message: errorMessage)) + } + }, receiveValue: { [weak self] result in + log.info("User \(result.user.uid) signed-up successfully. 🎉") + self?.authenticationState.send(.loggedIn) + self?.sendEmailVerification() + }) + .store(in: &self.cancellables) + } + + public func signIn(email: String, password: String) { + self.loadingStatePublisher.send(true) + self.auth.signIn(withEmail: email, password: password) { [weak self] authResult, error in + self?.loadingStatePublisher.send(false) + if let error { + log.error("Sign-in failed: \(error.localizedDescription)") + let errorMessage = String(l10n.AuthManager.signInFailedError.characters) + self?.authenticationError.send(AuthenticationError.custom(message: errorMessage)) + } else if let user = authResult?.user { + log.info("User \(user.uid) signed-in successfully. 🎉") + self?.authenticationState.send(.loggedIn) + self?.emailVerificationState.send(user.isEmailVerified) + } + } + } + + public func signOut() { + do { + try self.auth.signOut() + self.authenticationState.send(.loggedOut) + log.info("User was successfully signed out.") + } catch { + log.error("Sign out failed: \(error.localizedDescription)") + let errorMessage = String(l10n.AuthManager.signOutFailedError.characters) + self.authenticationError.send(AuthenticationError.custom(message: errorMessage)) + } + } + + public func sendEmailVerification() { + guard let currentUser = auth.currentUser else { return } + currentUser.sendEmailVerification { [weak self] error in + if let error { + log.error("\(error.localizedDescription)") + let errorMessage = String(l10n.AuthManager.verificationEmailFailure.characters) + self?.authenticationError.send(AuthenticationError.custom(message: errorMessage)) + return + } + } + } + + // MARK: Internal + + var authenticationErrorPublisher: AnyPublisher { + self.authenticationError.eraseToAnyPublisher() + } + + var emailVerificationStatePublisher: AnyPublisher { + self.emailVerificationState.eraseToAnyPublisher() + } + + var isLoadingPublisher: AnyPublisher { + self.loadingStatePublisher.eraseToAnyPublisher() + } + + // MARK: Private + + private let authenticationState = CurrentValueSubject(.unknown) + private let authenticationError = PassthroughSubject() + private let loadingStatePublisher = PassthroughSubject() + private let emailVerificationState = PassthroughSubject() + private let auth = Auth.auth() + private var cancellables = Set() + + private func updateAuthState(for user: User?) { + guard let user else { + self.authenticationState.send(.loggedOut) + return + } + self.authenticationState.send(.loggedIn) + self.emailVerificationState.send(user.isEmailVerified) + } +} diff --git a/Modules/AccountKit/Sources/Authentication/AuthManagerViewModel.swift b/Modules/AccountKit/Sources/Authentication/AuthManagerViewModel.swift new file mode 100644 index 0000000000..b115bc26ed --- /dev/null +++ b/Modules/AccountKit/Sources/Authentication/AuthManagerViewModel.swift @@ -0,0 +1,117 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import Foundation +import LocalizationKit + +// MARK: - AuthManagerViewModel + +public class AuthManagerViewModel: ObservableObject { + // MARK: Lifecycle + + public init() { + self.subscribeToAuthManager() + } + + // MARK: Public + + public static let shared = AuthManagerViewModel() + + // MARK: - User + + @Published public var userAuthenticationState: AuthManager.AuthenticationState = .unknown + @Published public var userAction: AuthManager.UserAction? + @Published public var userEmailIsVerified = false + + // MARK: - Alerts + + @Published public var errorMessage: String = "" + @Published public var showErrorAlert = false + @Published public var actionRequestMessage: String = "" + @Published public var showactionRequestAlert = false + @Published public var isLoading: Bool = false + + public func resetErrorMessage() { + self.errorMessage = "" + self.showErrorAlert = false + } + + // MARK: Private + + private let authManager: AuthManager = .shared + private var cancellables = Set() + + private func subscribeToAuthManager() { + self.authManager.authenticationStatePublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] state in + self?.userAuthenticationState = state + self?.handleAuthenticationStateChange(state: state) + } + .store(in: &self.cancellables) + + self.authManager.authenticationErrorPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] error in + if let authError = error as? AuthManager.AuthenticationError { + self?.errorMessage = authError.localizedDescription + } else { + self?.errorMessage = error.localizedDescription + } + self?.showErrorAlert = true + } + .store(in: &self.cancellables) + + self.authManager.isLoadingPublisher + .receive(on: DispatchQueue.main) + .assign(to: &self.$isLoading) + + self.authManager.emailVerificationStatePublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] state in + self?.userEmailIsVerified = state + } + .store(in: &self.cancellables) + } + + private func handleAuthenticationStateChange(state: AuthManager.AuthenticationState) { + switch state { + case .loggedIn: + if self.userAction == .none { + self.actionRequestMessage = String(l10n.AuthManagerViewModel.unverifiedEmailNotification.characters) + self.showactionRequestAlert = true + } + self.resetErrorMessage() + case .loggedOut: + self.resetState() + case .unknown: + break + } + } + + private func resetState() { + self.userAction = .none + self.userEmailIsVerified = false + self.errorMessage = "" + self.actionRequestMessage = "" + self.showactionRequestAlert = false + self.showErrorAlert = false + } +} + +// MARK: - l10n.AuthManagerViewModel + +// swiftlint:disable line_length + +extension l10n { + enum AuthManagerViewModel { + static let unverifiedEmailNotification = LocalizedString("accountkit.auth_manager_view_model.unverified_email_notification", + bundle: AccountKitResources.bundle, + value: "Your email hasn't been verified yet. Please verify your email to avoid losing your data.", + comment: "Unverified email notification message") + } +} + +// swiftlint:enable line_length diff --git a/Modules/AccountKit/Sources/Authentication/AuthenticationError.swift b/Modules/AccountKit/Sources/Authentication/AuthenticationError.swift new file mode 100644 index 0000000000..02b346edf4 --- /dev/null +++ b/Modules/AccountKit/Sources/Authentication/AuthenticationError.swift @@ -0,0 +1,20 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +extension AuthManager { + enum AuthenticationError: Error { + case custom(message: String) + + // MARK: Internal + + var localizedDescription: String { + switch self { + case let .custom(message): + message + } + } + } +} diff --git a/Modules/AccountKit/Sources/Database/DatabaseCollections.swift b/Modules/AccountKit/Sources/Database/DatabaseCollections.swift new file mode 100644 index 0000000000..4f264dafe8 --- /dev/null +++ b/Modules/AccountKit/Sources/Database/DatabaseCollections.swift @@ -0,0 +1,11 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +public enum DatabaseCollection: String { + case rootAccounts + case caregivers + case carereceivers +} diff --git a/Modules/AccountKit/Sources/Database/DatabaseErrors.swift b/Modules/AccountKit/Sources/Database/DatabaseErrors.swift new file mode 100644 index 0000000000..a6813c3b2d --- /dev/null +++ b/Modules/AccountKit/Sources/Database/DatabaseErrors.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +public enum DatabaseError: Error { + case customError(String) + case documentNotFound + case decodeError + case encodeError +} diff --git a/Modules/AccountKit/Sources/Database/DatabaseOperations.swift b/Modules/AccountKit/Sources/Database/DatabaseOperations.swift new file mode 100644 index 0000000000..69f1f1e81d --- /dev/null +++ b/Modules/AccountKit/Sources/Database/DatabaseOperations.swift @@ -0,0 +1,151 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import FirebaseAuth +import FirebaseFirestore +import FirebaseFirestoreSwift + +public class DatabaseOperations { + // MARK: Lifecycle + + public init() { + // Just to expose the init publicly + } + + // MARK: Public + + public static let shared = DatabaseOperations() + + public func create(data: T, in collection: DatabaseCollection) -> AnyPublisher { + Future { promise in + let docRef = self.database.collection(collection.rawValue).document() + var documentData = data + documentData.rootOwnerUid = Auth.auth().currentUser?.uid ?? "" + documentData.id = docRef.documentID + + do { + try docRef.setData(from: documentData) { error in + if let error { + log.error("\(error.localizedDescription)") + promise(.failure(DatabaseError.customError(error.localizedDescription))) + } else { + log.info("Document \(docRef.documentID) created successfully in \(collection). 🎉") + promise(.success(documentData)) + } + } + } catch { + log.error("\(error.localizedDescription)") + promise(.failure(DatabaseError.encodeError)) + } + } + .eraseToAnyPublisher() + } + + public func read(from collection: DatabaseCollection, documentID: String) -> AnyPublisher { + Future { promise in + let docRef = self.database.collection(collection.rawValue).document(documentID) + docRef.getDocument { document, error in + if let error { + log.error("\(error.localizedDescription)") + promise(.failure(error)) + } else { + do { + let object = try document?.data(as: T.self) + if let object { + log.info("Document \(String(describing: object.id)) fetched successfully from \(collection.rawValue). 🎉") + promise(.success(object)) + } else { + log.error("Document not found.") + promise(.failure(DatabaseError.documentNotFound)) + } + } catch { + log.error("\(error.localizedDescription)") + promise(.failure(error)) + } + } + } + } + .eraseToAnyPublisher() + } + + public func observeAll(from collection: DatabaseCollection) -> AnyPublisher<[T], Error> { + let subject = CurrentValueSubject<[T], Error>([]) + + if let existingListener = listenerRegistrations[collection.rawValue] { + existingListener.remove() + self.listenerRegistrations.removeValue(forKey: collection.rawValue) + } + + let listener = self.database.collection(collection.rawValue) + .whereField("root_owner_uid", isEqualTo: Auth.auth().currentUser?.uid ?? "") + .addSnapshotListener { querySnapshot, error in + if let error { + log.error("\(error.localizedDescription)") + subject.send(completion: .failure(error)) + } else if let querySnapshot { + let objects = querySnapshot.documents.compactMap { document -> T? in + try? document.data(as: T.self) + } + log.info("\(String(describing: objects.count)) \(collection.rawValue) documents fetched successfully. 🎉") + subject.send(objects) + } + } + + self.listenerRegistrations[collection.rawValue] = listener + + return subject.eraseToAnyPublisher() + } + + public func clearAllListeners() { + for (_, registration) in self.listenerRegistrations { + registration.remove() + } + self.listenerRegistrations.removeAll() + } + + public func update(data: some AccountDocument, in collection: DatabaseCollection) -> AnyPublisher { + Future { promise in + let docRef = self.database.collection(collection.rawValue).document(data.id!) + + do { + try docRef.setData(from: data, merge: true) { error in + if let error { + log.error("\(error.localizedDescription)") + promise(.failure(DatabaseError.customError(error.localizedDescription))) + } else { + log.info("Document \(String(describing: data.id!)) updated successfully in \(collection.rawValue). 🎉") + promise(.success(())) + } + } + } catch { + log.error("\(error.localizedDescription)") + promise(.failure(DatabaseError.encodeError)) + } + } + .eraseToAnyPublisher() + } + + public func delete(from collection: DatabaseCollection, documentID: String) -> AnyPublisher { + Future { promise in + let docRef = self.database.collection(collection.rawValue).document(documentID) + + docRef.delete { error in + if let error { + log.error("\(error.localizedDescription)") + promise(.failure(DatabaseError.customError(error.localizedDescription))) + } else { + log.info("Document \(String(describing: documentID)) deleted successfully from \(collection.rawValue). 🎉") + promise(.success(())) + } + } + } + .eraseToAnyPublisher() + } + + // MARK: Private + + private let database = Firestore.firestore() + private var listenerRegistrations = [String: ListenerRegistration]() +} diff --git a/Modules/AccountKit/Sources/Extensions/ColorScheme+Codable.swift b/Modules/AccountKit/Sources/Extensions/ColorScheme+Codable.swift new file mode 100644 index 0000000000..3a1cfc8173 --- /dev/null +++ b/Modules/AccountKit/Sources/Extensions/ColorScheme+Codable.swift @@ -0,0 +1,32 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// MARK: - ColorScheme + Codable, RawRepresentabl + +extension ColorScheme: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(String.self) + switch rawValue { + case "light": self = .light + case "dark": self = .dark + default: fatalError("Invalid ColorScheme") + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.rawValue) + } + + public var rawValue: String { + switch self { + case .light: return "light" + case .dark: return "dark" + @unknown default: return "light" + } + } +} diff --git a/Modules/AccountKit/Sources/Extensions/ColorTheme+Codable.swift b/Modules/AccountKit/Sources/Extensions/ColorTheme+Codable.swift new file mode 100644 index 0000000000..3a2a3ed9ce --- /dev/null +++ b/Modules/AccountKit/Sources/Extensions/ColorTheme+Codable.swift @@ -0,0 +1,19 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +public extension ColorTheme { + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(String.self) + + self = ColorTheme(rawValue: rawValue) ?? .darkBlue + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.rawValue) + } +} diff --git a/Modules/AccountKit/Sources/Extensions/JSONDecoderExtension.swift b/Modules/AccountKit/Sources/Extensions/JSONDecoderExtension.swift new file mode 100644 index 0000000000..4535e6c0fd --- /dev/null +++ b/Modules/AccountKit/Sources/Extensions/JSONDecoderExtension.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +extension JSONDecoder { + func decode(_: T.Type, fromJSONObject object: Any) throws -> T { + let data = try JSONSerialization.data(withJSONObject: object) + return try self.decode(T.self, from: data) + } +} diff --git a/Modules/AccountKit/Sources/Extensions/StringFormatValidationExtension.swift b/Modules/AccountKit/Sources/Extensions/StringFormatValidationExtension.swift new file mode 100644 index 0000000000..0fb2ed3a74 --- /dev/null +++ b/Modules/AccountKit/Sources/Extensions/StringFormatValidationExtension.swift @@ -0,0 +1,32 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +// Check if email or password format is correct +public extension String { + func isValidEmail() -> Bool { + let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let predicate = NSPredicate(format: "SELF MATCHES %@", regex) + return predicate.evaluate(with: self) && !self.isEmpty + } + + func isInvalidEmail(checkEmpty: Bool = true) -> Bool { + guard checkEmpty else { + return !self.isValidEmail() + } + return !self.isValidEmail() || self.isEmpty + } + + func isValidPassword() -> Bool { + // 8 characters minimum, must contain at least one number and one Capital letter + let regex = "^[^\\s]{12,}$" + let predicate = NSPredicate(format: "SELF MATCHES %@", regex) + return predicate.evaluate(with: self) && !self.isEmpty + } + + func isInvalidPassword() -> Bool { + !self.isValidPassword() || self.isEmpty + } +} diff --git a/Modules/AccountKit/Sources/Managers/CaregiverManager.swift b/Modules/AccountKit/Sources/Managers/CaregiverManager.swift new file mode 100644 index 0000000000..a66926c1e7 --- /dev/null +++ b/Modules/AccountKit/Sources/Managers/CaregiverManager.swift @@ -0,0 +1,118 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine + +public class CaregiverManager { + // MARK: Lifecycle + + private init() { + self.initializeCaregiversListener() + } + + // MARK: Public + + public static let shared = CaregiverManager() + + public func initializeCaregiversListener() { + self.dbOps.observeAll(from: .caregivers) + .sink(receiveCompletion: { [weak self] completion in + if case let .failure(error) = completion { + self?.fetchErrorSubject.send(error) + } + }, receiveValue: { [weak self] fetchedCaregivers in + self?.caregiverList.send(fetchedCaregivers) + }) + .store(in: &self.cancellables) + } + + public func fetchCaregiver(documentID: String) { + self.dbOps.read(from: .caregivers, documentID: documentID) + .sink(receiveCompletion: { [weak self] completion in + if case let .failure(error) = completion { + self?.fetchErrorSubject.send(error) + } + }, receiveValue: { [weak self] fetchedCaregiver in + self?.currentCaregiver.send(fetchedCaregiver) + }) + .store(in: &self.cancellables) + } + + public func createCaregiver(caregiver: Caregiver) -> AnyPublisher { + self.dbOps.create(data: caregiver, in: .caregivers) + .flatMap { [weak self] createdCaregiver -> AnyPublisher in + guard self != nil else { + return Fail(error: DatabaseError.customError("Unexpected Nil Value")).eraseToAnyPublisher() + } + + return Just(createdCaregiver) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + } + .handleEvents(receiveOutput: { [weak self] _ in + self?.initializeCaregiversListener() + }) + .eraseToAnyPublisher() + } + + public func updateCaregiver(caregiver: inout Caregiver) { + caregiver.lastEditedAt = nil + let documentID = caregiver.id! + self.dbOps.update(data: caregiver, in: .caregivers) + .sink(receiveCompletion: { completion in + if case let .failure(error) = completion { + self.fetchErrorSubject.send(error) + } + }, receiveValue: { _ in + self.fetchCaregiver(documentID: documentID) + }) + .store(in: &self.cancellables) + } + + public func deleteCaregiver(documentID: String) { + self.dbOps.delete(from: .caregivers, documentID: documentID) + .sink(receiveCompletion: { completion in + if case let .failure(error) = completion { + self.fetchErrorSubject.send(error) + } + }, receiveValue: { + // Nothing to do + }) + .store(in: &self.cancellables) + } + + public func setCurrentCaregiver(to caregiver: Caregiver) { + self.currentCaregiver.send(caregiver) + } + + public func resetData() { + self.currentCaregiver.send(nil) + self.caregiverList.send([]) + self.dbOps.clearAllListeners() + self.cancellables.forEach { $0.cancel() } + self.cancellables.removeAll() + } + + // MARK: Internal + + var currentCaregiverPublisher: AnyPublisher { + self.currentCaregiver.eraseToAnyPublisher() + } + + var caregiversPublisher: AnyPublisher<[Caregiver], Never> { + self.caregiverList.eraseToAnyPublisher() + } + + var fetchErrorPublisher: AnyPublisher { + self.fetchErrorSubject.eraseToAnyPublisher() + } + + // MARK: Private + + private var caregiverList = CurrentValueSubject<[Caregiver], Never>([]) + private var currentCaregiver = CurrentValueSubject(nil) + private var fetchErrorSubject = PassthroughSubject() + private let dbOps = DatabaseOperations.shared + private var cancellables = Set() +} diff --git a/Modules/AccountKit/Sources/Managers/CaregiverManagerViewModel.swift b/Modules/AccountKit/Sources/Managers/CaregiverManagerViewModel.swift new file mode 100644 index 0000000000..793865a313 --- /dev/null +++ b/Modules/AccountKit/Sources/Managers/CaregiverManagerViewModel.swift @@ -0,0 +1,66 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import SwiftUI + +public class CaregiverManagerViewModel: ObservableObject { + // MARK: Lifecycle + + public init() { + self.subscribeToManager() + } + + // MARK: Public + + @Published public var caregivers: [Caregiver] = [] + @Published public var currentCaregiver: Caregiver? + @Published public var errorMessage: String = "" + @Published public var showErrorAlert = false + + // MARK: Private + + private var cancellables = Set() + private let caregiverManager = CaregiverManager.shared + + private func subscribeToManager() { + self.caregiverManager.caregiversPublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] fetchedCaregivers in + self?.caregivers = fetchedCaregivers + }) + .store(in: &self.cancellables) + + self.caregiverManager.currentCaregiverPublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] caregiver in + self?.currentCaregiver = caregiver + }) + .store(in: &self.cancellables) + + self.caregiverManager.fetchErrorPublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] error in + self?.handleError(error) + }) + .store(in: &self.cancellables) + } + + private func handleError(_ error: Error) { + if let databaseError = error as? DatabaseError { + switch databaseError { + case let .customError(message): + self.errorMessage = message + case .documentNotFound: + self.errorMessage = "The requested caregiver could not be found. Consider deleting this profile." + case .decodeError: + self.errorMessage = "There was an error decoding the data." + case .encodeError: + self.errorMessage = "There was an error encoding the data." + } + } else { + self.errorMessage = "An unknown error occurred: \(error.localizedDescription)" + } + } +} diff --git a/Modules/AccountKit/Sources/Managers/CarereceiverManager.swift b/Modules/AccountKit/Sources/Managers/CarereceiverManager.swift new file mode 100644 index 0000000000..73c64c8889 --- /dev/null +++ b/Modules/AccountKit/Sources/Managers/CarereceiverManager.swift @@ -0,0 +1,117 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine + +public class CarereceiverManager { + // MARK: Lifecycle + + private init() { + self.initializeCarereceiversListener() + } + + // MARK: Public + + public static let shared = CarereceiverManager() + + public func initializeCarereceiversListener() { + self.dbOps.observeAll(from: .carereceivers) + .sink(receiveCompletion: { [weak self] completion in + if case let .failure(error) = completion { + self?.fetchErrorSubject.send(error) + } + }, receiveValue: { [weak self] fetchedCarereceivers in + self?.carereceiverList.send(fetchedCarereceivers) + }) + .store(in: &self.cancellables) + } + + public func fetchCarereceiver(documentID: String) { + self.dbOps.read(from: .carereceivers, documentID: documentID) + .sink(receiveCompletion: { [weak self] completion in + if case let .failure(error) = completion { + self?.fetchErrorSubject.send(error) + } + }, receiveValue: { [weak self] fetchedCarereceiver in + self?.currentCarereceiver.send(fetchedCarereceiver) + }) + .store(in: &self.cancellables) + } + + public func createCarereceiver(carereceiver: Carereceiver) -> AnyPublisher { + self.dbOps.create(data: carereceiver, in: .carereceivers) + .flatMap { [weak self] createdCarereceiver -> AnyPublisher in + guard self != nil else { + return Fail(error: DatabaseError.customError("Unexpected Nil Value")).eraseToAnyPublisher() + } + + return Just(createdCarereceiver) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + } + .handleEvents(receiveOutput: { [weak self] _ in + self?.initializeCarereceiversListener() + }) + .eraseToAnyPublisher() + } + + public func updateCarereceiver(carereceiver: inout Carereceiver) { + carereceiver.lastEditedAt = nil + self.dbOps.update(data: carereceiver, in: .carereceivers) + .sink(receiveCompletion: { completion in + if case let .failure(error) = completion { + self.fetchErrorSubject.send(error) + } + }, receiveValue: { + // Nothing to do + }) + .store(in: &self.cancellables) + } + + public func deleteCarereceiver(documentID: String) { + self.dbOps.delete(from: .carereceivers, documentID: documentID) + .sink(receiveCompletion: { completion in + if case let .failure(error) = completion { + self.fetchErrorSubject.send(error) + } + }, receiveValue: { + // Nothing to do + }) + .store(in: &self.cancellables) + } + + public func setCurrentCarereceiver(to carereceiver: Carereceiver) { + self.currentCarereceiver.send(carereceiver) + } + + public func resetData() { + self.currentCarereceiver.send(nil) + self.carereceiverList.send([]) + self.dbOps.clearAllListeners() + self.cancellables.forEach { $0.cancel() } + self.cancellables.removeAll() + } + + // MARK: Internal + + var carereceiversPublisher: AnyPublisher<[Carereceiver], Never> { + self.carereceiverList.eraseToAnyPublisher() + } + + var currentCarereceiverPublisher: AnyPublisher { + self.currentCarereceiver.eraseToAnyPublisher() + } + + var fetchErrorPublisher: AnyPublisher { + self.fetchErrorSubject.eraseToAnyPublisher() + } + + // MARK: Private + + private var carereceiverList = CurrentValueSubject<[Carereceiver], Never>([]) + private var currentCarereceiver = CurrentValueSubject(nil) + private var fetchErrorSubject = PassthroughSubject() + private let dbOps = DatabaseOperations.shared + private var cancellables = Set() +} diff --git a/Modules/AccountKit/Sources/Managers/CarereceiverManagerViewModel.swift b/Modules/AccountKit/Sources/Managers/CarereceiverManagerViewModel.swift new file mode 100644 index 0000000000..bff64c8fbb --- /dev/null +++ b/Modules/AccountKit/Sources/Managers/CarereceiverManagerViewModel.swift @@ -0,0 +1,66 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import SwiftUI + +public class CarereceiverManagerViewModel: ObservableObject { + // MARK: Lifecycle + + public init() { + self.subscribeToManager() + } + + // MARK: Public + + @Published public var carereceivers: [Carereceiver] = [] + @Published public var currentCarereceiver: Carereceiver? + @Published public var errorMessage: String = "" + @Published public var showErrorAlert = false + + // MARK: Private + + private var cancellables = Set() + private let carereceiverManager = CarereceiverManager.shared + + private func subscribeToManager() { + self.carereceiverManager.carereceiversPublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] fetchedCarereceivers in + self?.carereceivers = fetchedCarereceivers + }) + .store(in: &self.cancellables) + + self.carereceiverManager.currentCarereceiverPublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] fetchedCarereceiver in + self?.currentCarereceiver = fetchedCarereceiver + }) + .store(in: &self.cancellables) + + self.carereceiverManager.fetchErrorPublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] error in + self?.handleError(error) + }) + .store(in: &self.cancellables) + } + + private func handleError(_ error: Error) { + if let databaseError = error as? DatabaseError { + switch databaseError { + case let .customError(message): + self.errorMessage = message + case .documentNotFound: + self.errorMessage = "The requested carereceiver could not be found. Consider deleting this profile." + case .decodeError: + self.errorMessage = "There was an error decoding the data." + case .encodeError: + self.errorMessage = "There was an error encoding the data." + } + } else { + self.errorMessage = "An unknown error occurred: \(error.localizedDescription)" + } + } +} diff --git a/Modules/AccountKit/Sources/Managers/RootAccountManager.swift b/Modules/AccountKit/Sources/Managers/RootAccountManager.swift new file mode 100644 index 0000000000..addbbeac93 --- /dev/null +++ b/Modules/AccountKit/Sources/Managers/RootAccountManager.swift @@ -0,0 +1,42 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine + +public class RootAccountManager { + // MARK: Lifecycle + + private init() { + // Nothing to do + } + + // MARK: Public + + public static let shared = RootAccountManager() + + public func createRootAccount(rootAccount: RootAccount) { + self.dbOps.create(data: rootAccount, in: .rootAccounts) + .sink(receiveCompletion: { completion in + if case let .failure(error) = completion { + self.fetchErrorSubject.send(error) + } + }, receiveValue: { _ in + // Deal with newly created RootAccount in the Back-Office + }) + .store(in: &self.cancellables) + } + + // MARK: Internal + + var fetchErrorPublisher: AnyPublisher { + self.fetchErrorSubject.eraseToAnyPublisher() + } + + // MARK: Private + + private let dbOps = DatabaseOperations.shared + private var cancellables = Set() + + private var fetchErrorSubject = PassthroughSubject() +} diff --git a/Modules/AccountKit/Sources/Managers/RootAccountManagerViewModel.swift b/Modules/AccountKit/Sources/Managers/RootAccountManagerViewModel.swift new file mode 100644 index 0000000000..83d7de1c7a --- /dev/null +++ b/Modules/AccountKit/Sources/Managers/RootAccountManagerViewModel.swift @@ -0,0 +1,36 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import SwiftUI + +public class RootAccountManagerViewModel: ObservableObject { + // MARK: Lifecycle + + public init() { + self.subscribeToManager() + } + + // MARK: Public + + @Published public var errorMessage: String = "" + + // MARK: Private + + private var cancellables = Set() + private let rootAccountManager = RootAccountManager.shared + + private func subscribeToManager() { + self.rootAccountManager.fetchErrorPublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] error in + self?.handleError(error) + }) + .store(in: &self.cancellables) + } + + private func handleError(_ error: Error) { + self.errorMessage = "An unknown error occurred: \(error.localizedDescription)" + } +} diff --git a/Modules/AccountKit/Sources/Models/Avatar.swift b/Modules/AccountKit/Sources/Models/Avatar.swift new file mode 100644 index 0000000000..7a9a45bc5e --- /dev/null +++ b/Modules/AccountKit/Sources/Models/Avatar.swift @@ -0,0 +1,138 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import LocalizationKit +import LogKit +import SwiftUI +import Version +import Yams + +// MARK: - Avatars + +public class Avatars: Codable { + // MARK: Lifecycle + + private init() { + self.container = Self.loadAvatars() + } + + // MARK: Public + + public static var categories: [AvatarCategory] { + shared.container.categories + } + + public static var version: Version { + shared.container.version + } + + public static func avatar(for id: String) -> AvatarCategory? { + self.categories.first(where: { $0.id == id }) + } + + public static func iconToUIImage(icon: String) -> UIImage { + guard let image = UIImage(named: "\(icon).avatars.png", in: .module, with: nil) else { + log.error("No image found for icon \(icon)") + fatalError("💥 No image found for icon \(icon)") + } + return image + } + + // MARK: Private + + private struct AvatarsContainer: Codable { + let version: Version + let categories: [AvatarCategory] + } + + private static let shared: Avatars = .init() + + private let container: AvatarsContainer + + private static func loadAvatars() -> AvatarsContainer { + if let fileURL = Bundle.module.url(forResource: "avatars", withExtension: "yml") { + do { + let yamlString = try String(contentsOf: fileURL, encoding: .utf8) + let container = try YAMLDecoder().decode(AvatarsContainer.self, from: yamlString) + return container + } catch { + log.error("Failed to read YAML file: \(error)") + return AvatarsContainer(version: .init(1, 0, 0), categories: []) + } + } else { + log.error("Avatars.yml not found") + return AvatarsContainer(version: .init(1, 0, 0), categories: []) + } + } +} + +// MARK: - AvatarCategory + +public struct AvatarCategory: Codable, Identifiable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(String.self, forKey: .id) + self.visible = try container.decode(Bool.self, forKey: .visible) + + self.l10n = try container.decode([AvatarCategory.Localization].self, forKey: .l10n) + + let availableLocales = self.l10n.map(\.locale) + + let currentLocale = availableLocales.first(where: { + $0.language.languageCode == LocalizationKit.l10n.language + }) ?? Locale(identifier: "en_US") + + self.name = self.l10n.first(where: { $0.locale == currentLocale })!.name + self.avatars = try container.decode([String].self, forKey: .avatars) + } + + // MARK: Public + + public let id: String + public let visible: Bool + public let name: String + public let avatars: [String] + + // MARK: Private + + private let l10n: [Localization] +} + +// MARK: Equatable + +extension AvatarCategory: Equatable { + public static func == (lhs: AvatarCategory, rhs: AvatarCategory) -> Bool { + lhs.id == rhs.id + } +} + +// MARK: Hashable + +extension AvatarCategory: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } +} + +// MARK: AvatarCategory.Localization + +public extension AvatarCategory { + struct Localization: Codable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.locale = try Locale(identifier: container.decode(String.self, forKey: .locale)) + self.name = try container.decode(String.self, forKey: .name) + } + + // MARK: Internal + + let locale: Locale + let name: String + } +} diff --git a/Modules/AccountKit/Sources/Models/CaregiverModel/Caregiver+Init.swift b/Modules/AccountKit/Sources/Models/CaregiverModel/Caregiver+Init.swift new file mode 100644 index 0000000000..d175ca1e50 --- /dev/null +++ b/Modules/AccountKit/Sources/Models/CaregiverModel/Caregiver+Init.swift @@ -0,0 +1,30 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +public extension Caregiver { + init(id: String = "", + rootOwnerUid: String = "", + firstName: String = "", + lastName: String = "", + birthdate: String = "", + email: String = "", + avatar: String = "", + professions: [String] = [], + colorScheme: ColorScheme = .light, + colorTheme: ColorTheme = .darkBlue) + { + self.id = id + self.rootOwnerUid = rootOwnerUid + self.firstName = firstName + self.lastName = lastName + self.birthdate = birthdate + self.email = email + self.avatar = avatar + self.professions = professions + self.colorScheme = colorScheme + self.colorTheme = colorTheme + } +} diff --git a/Modules/AccountKit/Sources/Models/CaregiverModel/Caregiver+Mock.swift b/Modules/AccountKit/Sources/Models/CaregiverModel/Caregiver+Mock.swift new file mode 100644 index 0000000000..fe473247cb --- /dev/null +++ b/Modules/AccountKit/Sources/Models/CaregiverModel/Caregiver+Mock.swift @@ -0,0 +1,29 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftlint:disable all +// swift-format-ignore-file +// swiftformat:disable all + +import SwiftUI + +public extension Caregiver { + // swiftlint:disable line_length + static var mockCaregiversSet: [Caregiver] = + [ + Caregiver(id: UUID().uuidString, firstName: "Chantal", lastName: "Goya", avatar: Avatars.categories[0].avatars[2], professions: [Professions.list[6].id]), + Caregiver(id: UUID().uuidString, firstName: "Gaëtan", lastName: "Roussel", avatar: "boy1", professions: [Professions.list[9].id], colorScheme: .dark, colorTheme: .green), + Caregiver(id: UUID().uuidString, firstName: "Fabrizio", lastName: "Ferrari", avatar: Avatars.categories[4].avatars[1], professions: [Professions.list[10].id]), + Caregiver(id: UUID().uuidString, firstName: "Hakima", lastName: "Queen", avatar: "girl10", professions: [Professions.list[10].id, Professions.list[1].id]), + Caregiver(id: UUID().uuidString, firstName: "Eric", lastName: "Clapton", avatar: Avatars.categories[1].avatars[0], professions: [Professions.list[10].id]), + Caregiver(id: UUID().uuidString, firstName: "Razmo", lastName: "Kets", avatar: Avatars.categories[2].avatars[2], professions: [Professions.list[5].id], colorScheme: .dark, colorTheme: .orange), + Caregiver(id: UUID().uuidString, firstName: "Corinne", lastName: "Lepage", avatar: Avatars.categories[5].avatars[1], professions: [Professions.list[4].id]), + Caregiver(id: UUID().uuidString, firstName: "Alphonso", lastName: "Mango", avatar: Avatars.categories[4].avatars[1], professions: [Professions.list[0].id]), + Caregiver(id: UUID().uuidString, firstName: "Gargantua", lastName: "Pantagruel", avatar: Avatars.categories[3].avatars[2], professions: [Professions.list[2].id]), + ] + // swiftlint:enable line_length +} + +// swiftlint:enable all +// swiftformat:enable all diff --git a/Modules/AccountKit/Sources/Models/CaregiverModel/Caregiver.swift b/Modules/AccountKit/Sources/Models/CaregiverModel/Caregiver.swift new file mode 100644 index 0000000000..18e190a1e8 --- /dev/null +++ b/Modules/AccountKit/Sources/Models/CaregiverModel/Caregiver.swift @@ -0,0 +1,44 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import FirebaseFirestore +import SwiftUI + +// MARK: - Caregiver + +public struct Caregiver: AccountDocument { + // MARK: Public + + @ServerTimestamp public var createdAt: Date? + @ServerTimestamp public var lastEditedAt: Date? + + public var id: String? + public var rootOwnerUid: String + public var firstName: String + public var lastName: String + public var birthdate: String + public var email: String + public var avatar: String + public var professions: [String] + public var colorScheme: ColorScheme + public var colorTheme: ColorTheme + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case id + case rootOwnerUid = "root_owner_uid" + case createdAt = "created_at" + case lastEditedAt = "last_edited_at" + case firstName = "first_name" + case lastName = "last_name" + case birthdate + case email + case avatar + case professions + case colorScheme = "color_scheme" + case colorTheme = "color_theme" + } +} diff --git a/Modules/AccountKit/Sources/Models/CarereceiverModel/Carereceiver+Mock.swift b/Modules/AccountKit/Sources/Models/CarereceiverModel/Carereceiver+Mock.swift new file mode 100644 index 0000000000..47f3b4ad2d --- /dev/null +++ b/Modules/AccountKit/Sources/Models/CarereceiverModel/Carereceiver+Mock.swift @@ -0,0 +1,25 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftlint:disable all +// swift-format-ignore-file +// swiftformat:disable all + +import SwiftUI + +public extension Carereceiver { + // swiftlint:disable line_length + static var mockCarereceiversSet: [Carereceiver] = + [ + Carereceiver(id: UUID().uuidString, username: "Peet", avatar: Avatars.categories[2].avatars[2], reinforcer: .rainbow), + Carereceiver(id: UUID().uuidString, username: "Rounhaa", avatar: Avatars.categories[4].avatars[0], reinforcer: .sprinkles), + Carereceiver(id: UUID().uuidString, username: "Selug", avatar: Avatars.categories[5].avatars[1], reinforcer: .spinBlinkBlueViolet), + Carereceiver(id: UUID().uuidString, username: "Luther", avatar: Avatars.categories[1].avatars[2], reinforcer: .spinBlinkGreenOff), + Carereceiver(id: UUID().uuidString, username: "Abel", avatar: Avatars.categories[2].avatars[3], reinforcer: .fire), + ] + // swiftlint:enable line_length +} + +// swiftlint:enable all +// swiftformat:enable all diff --git a/Modules/AccountKit/Sources/Models/CarereceiverModel/Carereceiver+init.swift b/Modules/AccountKit/Sources/Models/CarereceiverModel/Carereceiver+init.swift new file mode 100644 index 0000000000..31dd3e9412 --- /dev/null +++ b/Modules/AccountKit/Sources/Models/CarereceiverModel/Carereceiver+init.swift @@ -0,0 +1,21 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import RobotKit +import SwiftUI + +public extension Carereceiver { + init(id: String = "", + rootOwnerUid: String = "", + username: String = "", + avatar: String = "", + reinforcer: Robot.Reinforcer = .rainbow) + { + self.id = id + self.rootOwnerUid = rootOwnerUid + self.username = username + self.avatar = avatar + self.reinforcer = reinforcer + } +} diff --git a/Modules/AccountKit/Sources/Models/CarereceiverModel/Carereceiver.swift b/Modules/AccountKit/Sources/Models/CarereceiverModel/Carereceiver.swift new file mode 100644 index 0000000000..1b86f1d8ca --- /dev/null +++ b/Modules/AccountKit/Sources/Models/CarereceiverModel/Carereceiver.swift @@ -0,0 +1,32 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import FirebaseFirestore +import RobotKit +import SwiftUI + +public struct Carereceiver: AccountDocument, Hashable { + // MARK: Public + + @ServerTimestamp public var createdAt: Date? + @ServerTimestamp public var lastEditedAt: Date? + + public var id: String? + public var rootOwnerUid: String + public var username: String + public var avatar: String + public var reinforcer: Robot.Reinforcer + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case id + case rootOwnerUid = "root_owner_uid" + case createdAt = "created_at" + case lastEditedAt = "last_edited_at" + case username + case avatar + case reinforcer + } +} diff --git a/Modules/AccountKit/Sources/Models/ColorTheme.swift b/Modules/AccountKit/Sources/Models/ColorTheme.swift new file mode 100644 index 0000000000..bd92cd6575 --- /dev/null +++ b/Modules/AccountKit/Sources/Models/ColorTheme.swift @@ -0,0 +1,32 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +public enum ColorTheme: String, CaseIterable, Codable { + case darkBlue + case blue + case purple + case red + case orange + case yellow + case green + case gray + + // MARK: Public + + public var color: Color { + switch self { + case .darkBlue: DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor + case .blue: .blue + case .purple: .purple + case .red: .red + case .orange: .orange + case .yellow: .yellow + case .green: .green + case .gray: .gray + } + } +} diff --git a/Modules/AccountKit/Sources/Models/Professions.swift b/Modules/AccountKit/Sources/Models/Professions.swift new file mode 100644 index 0000000000..c9123a30a3 --- /dev/null +++ b/Modules/AccountKit/Sources/Models/Professions.swift @@ -0,0 +1,129 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import LocalizationKit +import LogKit +import Version +import Yams + +// MARK: - Professions + +public class Professions: Codable { + // MARK: Lifecycle + + private init() { + self.container = Self.loadProfessions() + } + + // MARK: Public + + public static var list: [Profession] { + shared.container.list.sorted { $0.name.compare($1.name, locale: NSLocale.current) == .orderedAscending } + } + + public static var version: Version { + shared.container.version + } + + public static func profession(for id: String) -> Profession? { + self.list.first(where: { $0.id == id }) + } + + // MARK: Private + + private struct ProfessionsContainer: Codable { + let version: Version + let list: [Profession] + } + + private static let shared: Professions = .init() + + private let container: ProfessionsContainer + + private static func loadProfessions() -> ProfessionsContainer { + if let fileURL = Bundle.module.url(forResource: "professions", withExtension: "yml") { + do { + let yamlString = try String(contentsOf: fileURL, encoding: .utf8) + let container = try YAMLDecoder().decode(ProfessionsContainer.self, from: yamlString) + return container + } catch { + log.error("Failed to read YAML file: \(error)") + return ProfessionsContainer(version: .init(1, 0, 0), list: []) + } + } else { + log.error("professions.yml not found") + return ProfessionsContainer(version: .init(1, 0, 0), list: []) + } + } +} + +// MARK: - Profession + +public struct Profession: Codable, Identifiable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(String.self, forKey: .id) + + self.l10n = try container.decode([Profession.Localization].self, forKey: .l10n) + + let availableLocales = self.l10n.map(\.locale) + + let currentLocale = availableLocales.first(where: { + $0.language.languageCode == LocalizationKit.l10n.language + }) ?? Locale(identifier: "en_US") + + self.name = self.l10n.first(where: { $0.locale == currentLocale })!.name + self.description = self.l10n.first(where: { $0.locale == currentLocale })!.description + } + + // MARK: Public + + public let id: String + public let name: String + public let description: String + + // MARK: Private + + private let l10n: [Localization] +} + +// MARK: Equatable + +extension Profession: Equatable { + public static func == (lhs: Profession, rhs: Profession) -> Bool { + lhs.id == rhs.id + } +} + +// MARK: Hashable + +extension Profession: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } +} + +// MARK: Profession.Localization + +public extension Profession { + struct Localization: Codable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.locale = try Locale(identifier: container.decode(String.self, forKey: .locale)) + self.name = try container.decode(String.self, forKey: .name) + self.description = try container.decode(String.self, forKey: .description) + } + + // MARK: Internal + + let locale: Locale + let name: String + let description: String + } +} diff --git a/Modules/AccountKit/Sources/Models/RootAccount/RootAccount+AccountDocumentProtocol.swift b/Modules/AccountKit/Sources/Models/RootAccount/RootAccount+AccountDocumentProtocol.swift new file mode 100644 index 0000000000..c0f29ae195 --- /dev/null +++ b/Modules/AccountKit/Sources/Models/RootAccount/RootAccount+AccountDocumentProtocol.swift @@ -0,0 +1,35 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import FirebaseFirestore +import SwiftUI + +// MARK: - AccountDocument + +public protocol AccountDocument: Codable, Identifiable { + var id: String? { get set } + var rootOwnerUid: String { get set } + var createdAt: Date? { get set } + var lastEditedAt: Date? { get set } +} + +// MARK: - RootAccount + +public struct RootAccount: AccountDocument { + // MARK: Public + + public var id: String? + @ServerTimestamp public var createdAt: Date? + @ServerTimestamp public var lastEditedAt: Date? + public var rootOwnerUid: String + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case id + case rootOwnerUid = "root_owner_uid" + case createdAt = "created_at" + case lastEditedAt = "last_edited_at" + } +} diff --git a/Modules/AccountKit/Sources/Models/RootAccount/RootAccount+Init.swift b/Modules/AccountKit/Sources/Models/RootAccount/RootAccount+Init.swift new file mode 100644 index 0000000000..055569d5b7 --- /dev/null +++ b/Modules/AccountKit/Sources/Models/RootAccount/RootAccount+Init.swift @@ -0,0 +1,14 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +public extension RootAccount { + init(id: String? = "", + rootOwnerUid: String = "") + { + self.id = id + self.rootOwnerUid = rootOwnerUid + } +} diff --git a/Modules/AccountKit/Tests/AccountKit_Tests.swift b/Modules/AccountKit/Tests/AccountKit_Tests.swift new file mode 100644 index 0000000000..087f8e4786 --- /dev/null +++ b/Modules/AccountKit/Tests/AccountKit_Tests.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +final class AccountKit_Tests: XCTestCase { + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/Modules/BLEKit/Examples/BLEKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Modules/BLEKit/Examples/BLEKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Modules/BLEKit/Examples/BLEKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/BLEKit/Examples/BLEKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Modules/BLEKit/Examples/BLEKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Modules/BLEKit/Examples/BLEKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/BLEKit/Examples/BLEKitExample/Resources/Assets.xcassets/Contents.json b/Modules/BLEKit/Examples/BLEKitExample/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/BLEKit/Examples/BLEKitExample/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/BLEKit/Examples/BLEKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Modules/BLEKit/Examples/BLEKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/BLEKit/Examples/BLEKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/BLEKit/Examples/BLEKitExample/Sources/MainApp.swift b/Modules/BLEKit/Examples/BLEKitExample/Sources/MainApp.swift new file mode 100644 index 0000000000..c8524f0f49 --- /dev/null +++ b/Modules/BLEKit/Examples/BLEKitExample/Sources/MainApp.swift @@ -0,0 +1,19 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import SwiftUI + +@main +struct BLEKitExample: App { + var bleManager: BLEManager = .live() + + var body: some Scene { + WindowGroup { + NavigationStack { + ContentView(bleManager: self.bleManager) + } + } + } +} diff --git a/Modules/BLEKit/Examples/BLEKitExample/Sources/Mocks/RobotListViewModel+Mock.swift b/Modules/BLEKit/Examples/BLEKitExample/Sources/Mocks/RobotListViewModel+Mock.swift new file mode 100644 index 0000000000..5376a6a21d --- /dev/null +++ b/Modules/BLEKit/Examples/BLEKitExample/Sources/Mocks/RobotListViewModel+Mock.swift @@ -0,0 +1,20 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import Combine +import SwiftUI + +public extension RobotListViewModel { + // TODO(@ladislas): create protocol and mock RobotDiscoveryModel + static func mock() -> RobotListViewModel { + RobotListViewModel(availableRobots: [ + // RobotDiscovery.mock(), + // RobotDiscovery.mock(), + // RobotDiscovery.mock(), + // RobotDiscovery.mock(), + // RobotDiscovery.mock(), + ]) + } +} diff --git a/Modules/BLEKit/Examples/BLEKitExample/Sources/ViewModels/RobotListViewModel.swift b/Modules/BLEKit/Examples/BLEKitExample/Sources/ViewModels/RobotListViewModel.swift new file mode 100644 index 0000000000..02d6c8a8f9 --- /dev/null +++ b/Modules/BLEKit/Examples/BLEKitExample/Sources/ViewModels/RobotListViewModel.swift @@ -0,0 +1,111 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import Combine +import SwiftUI + +@MainActor +public class RobotListViewModel: ObservableObject { + // MARK: Lifecycle + + // MARK: - Public functions + + public init(bleManager: BLEManager) { + self.bleManager = bleManager + self.subscribeToScanningStatus() + } + + // MARK: - Private functions + + init(availableRobots: [RobotDiscoveryModel]) { + self.bleManager = BLEManager.live() + self.robotDiscoveries = availableRobots + } + + // MARK: Public + + public func scanForPeripherals() { + if !self.bleManager.isScanning.value { + print("Start scanning") + self.scanForRobotsTask = self.bleManager.scanForRobots() + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { completion in + if case let .failure(error) = completion { + print("💥 ERROR: \(error)") + } + }, + receiveValue: { robotDiscoveries in + self.robotDiscoveries = robotDiscoveries + } + ) + } else { + print("Stop scanning") + // TODO(@ladislas): do not reset to try and connect --> handle errors for real + self.robotDiscoveries = [] + self.scanForRobotsTask?.cancel() + self.selectedRobotDiscovery = nil + } + } + + public func connectToSelectedPeripheral() { + guard let selectedRobotDiscovery else { return } + print("Connecting to \(selectedRobotDiscovery.name)") + self.bleManager.connect(selectedRobotDiscovery) + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { completion in + if case let .failure(error) = completion { + print("💥 ERROR: \(error)") + } + }, + receiveValue: { [weak self] connectedRobotPeripheral in + guard let self else { return } + self.connectedRobotDiscovery = self.selectedRobotDiscovery + self.connectedRobotPeripheral = connectedRobotPeripheral + } + ) + .store(in: &self.cancellables) + } + + public func disconnectFromConnectedPeripheral() { + print("Disconnecting") + self.bleManager.disconnect() + self.connectedRobotDiscovery = nil + self.connectedRobotPeripheral = nil + if !self.isScanning { + self.robotDiscoveries = [] + } + } + + // MARK: Internal + + // MARK: - Private variables + + let bleManager: BLEManager + var cancellables: Set = [] + var scanForRobotsTask: AnyCancellable? + + // MARK: - Published variables + + @Published var selectedRobotDiscovery: RobotDiscoveryModel? + // TODO(@ladislas): are they both needed? + @Published var connectedRobotDiscovery: RobotDiscoveryModel? + @Published var connectedRobotPeripheral: RobotPeripheral? + @Published var robotDiscoveries: [RobotDiscoveryModel] = [] + @Published var isScanning: Bool = false + + // MARK: Private + + private func subscribeToScanningStatus() { + self.bleManager.isScanning + .receive(on: DispatchQueue.main) + .sink { [weak self] status in + guard let self else { return } + self.isScanning = status + } + .store(in: &self.cancellables) + } +} diff --git a/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/ConnectButton.swift b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/ConnectButton.swift new file mode 100644 index 0000000000..1acb9ba262 --- /dev/null +++ b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/ConnectButton.swift @@ -0,0 +1,102 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import Combine +import SwiftUI + +// MARK: - ConnectButton + +struct ConnectButton: View { + // MARK: Internal + + // MARK: - Environment variables + + @EnvironmentObject var robotListViewModel: RobotListViewModel + + // MARK: - Public views + + var body: some View { + Button { + if self.robotListViewModel.connectedRobotPeripheral == nil, self.robotListViewModel.selectedRobotDiscovery != nil { + self.connectToRobot() + } else { + self.disconnectFromRobot() + } + } label: { + if self.robotListViewModel.connectedRobotPeripheral == nil { + self.disconnectedView + } else { + self.connectedView + } + } + .opacity( + (self.robotListViewModel.connectedRobotPeripheral == nil && self.robotListViewModel.selectedRobotDiscovery == nil) + ? 0.5 : 1.0 + ) + .disabled( + self.robotListViewModel.connectedRobotPeripheral == nil && self.robotListViewModel.selectedRobotDiscovery == nil) + } + + // MARK: Private + + // MARK: - Private views + + private var connectedView: some View { + HStack(spacing: 10) { + Text("Disconnect") + .font(.headline) + Image(systemName: "link.circle") + .font(.title) + } + .foregroundColor(.red) + .frame(height: 50) + .frame(maxWidth: .infinity) + .background(.white) + .cornerRadius(10) + .overlay { + RoundedRectangle(cornerRadius: 10) + .stroke(.red, lineWidth: 2) + } + } + + private var disconnectedView: some View { + HStack(spacing: 10) { + Text( + { () -> String in + guard let name = robotListViewModel.selectedRobotDiscovery?.name else { + return "Select a robot to connect" + } + return "Connect to \(name)" + }() + ) + .monospacedDigit() + .font(.headline) + Image(systemName: "link.circle.fill") + .font(.title) + } + .foregroundColor(.white) + .frame(height: 50) + .frame(maxWidth: .infinity) + .background(.green) + .cornerRadius(10) + } + + private func connectToRobot() { + self.robotListViewModel.connectToSelectedPeripheral() + } + + private func disconnectFromRobot() { + self.robotListViewModel.disconnectFromConnectedPeripheral() + } +} + +// MARK: - ConnectButton_Previews + +struct ConnectButton_Previews: PreviewProvider { + static var previews: some View { + ConnectButton() + .environmentObject(RobotListViewModel.mock()) + } +} diff --git a/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/ContentView.swift b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/ContentView.swift new file mode 100644 index 0000000000..b8c10e52a2 --- /dev/null +++ b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/ContentView.swift @@ -0,0 +1,74 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import SwiftUI + +// MARK: - ContentView + +struct ContentView: View { + // MARK: Lifecycle + + // MARK: - Public functions + + init(bleManager: BLEManager) { + // ? StateObject dependency injection pattern as described here: + // https://developer.apple.com/documentation/swiftui/stateobject#Initialize-state-objects-using-external-data + _robotListViewModel = StateObject(wrappedValue: RobotListViewModel(bleManager: bleManager)) + } + + // MARK: Internal + + // MARK: - Views + + var body: some View { + VStack { + List(self.robotListViewModel.robotDiscoveries) { robotDiscovery in + RobotDiscoveryView(discovery: robotDiscovery) + .contentShape(Rectangle()) + .onTapGesture { + if robotDiscovery == self.robotListViewModel.selectedRobotDiscovery { + print("Unselect \(robotDiscovery)") + self.robotListViewModel.selectedRobotDiscovery = nil + + } else { + print("Select \(robotDiscovery)") + self.robotListViewModel.selectedRobotDiscovery = robotDiscovery + } + } + } + .disabled(self.robotListViewModel.connectedRobotPeripheral != nil) + .listStyle(.plain) + .padding() + + Divider() + .padding([.leading, .trailing], 30) + + VStack(spacing: 10) { + ScanButton() + ConnectButton() + SendDataButton() + } + .padding(30) + } + .navigationTitle("BLEKit Example App") + .environmentObject(self.robotListViewModel) + } + + // MARK: Private + + // MARK: - Environment variables + + @StateObject private var robotListViewModel: RobotListViewModel +} + +// MARK: - ContentView_Previews + +struct ContentView_Previews: PreviewProvider { + static let bleManager = BLEManager.live() + + static var previews: some View { + ContentView(bleManager: bleManager) + } +} diff --git a/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/RobotDiscoveryView.swift b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/RobotDiscoveryView.swift new file mode 100644 index 0000000000..70664d9d98 --- /dev/null +++ b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/RobotDiscoveryView.swift @@ -0,0 +1,77 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import SwiftUI + +struct RobotDiscoveryView: View { + // MARK: Lifecycle + + public init(discovery: RobotDiscoveryModel) { + self.discovery = discovery + } + + // MARK: Internal + + var body: some View { + HStack(spacing: 20) { + Image( + systemName: { + if self.robotListViewModel.selectedRobotDiscovery == self.discovery { + return "checkmark.circle.fill" + } + + if self.robotListViewModel.connectedRobotDiscovery == self.discovery { + return "checkmark.circle.fill" + } + + return "circle" + }() + ) + .font(.system(size: 40)) + .foregroundColor( + { + if self.robotListViewModel.connectedRobotDiscovery == self.discovery { + return .green + } + + if self.robotListViewModel.selectedRobotDiscovery == self.discovery { + return .accentColor + } + + return .gray + }() + ) + .animation(.easeIn(duration: 0.25), value: self.robotListViewModel.selectedRobotDiscovery == self.discovery) + + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 40) { + VStack(alignment: .leading) { + Text(self.discovery.name) + .font(.headline) + Text("v\(self.discovery.osVersion)") + } + + Spacer() + + VStack(alignment: .leading) { + Text("Battery: \(self.discovery.battery)") + Text("Charging: \(self.discovery.isCharging ? "Yes" : "No")") + .foregroundColor(self.discovery.isCharging ? .green : .red) + } + + Spacer() + } + } + } + } + + // MARK: Private + + private var discovery: RobotDiscoveryModel + + // MARK: - Environment variables + + @EnvironmentObject private var robotListViewModel: RobotListViewModel +} diff --git a/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/ScanButton.swift b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/ScanButton.swift new file mode 100644 index 0000000000..8681f5fa9e --- /dev/null +++ b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/ScanButton.swift @@ -0,0 +1,59 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import SwiftUI + +// MARK: - ScanButton + +struct ScanButton: View { + // MARK: - Environment variables + + @EnvironmentObject var robotListViewModel: RobotListViewModel + + // MARK: - Public views + + var body: some View { + Button { + self.robotListViewModel.scanForPeripherals() + } label: { + Group { + HStack(spacing: 10) { + if !self.robotListViewModel.isScanning { + Text("Start scanning") + .font(.headline) + .foregroundColor(.white) + + } else { + ZStack(alignment: .leading) { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .offset(x: -25) + Text("Stop scanning") + .font(.headline) + .foregroundColor(.white) + } + } + Image(systemName: "magnifyingglass.circle.fill") + .font(.title) + .foregroundColor(.white) + } + } + .frame(height: 50) + .frame(maxWidth: .infinity) + .background(!self.robotListViewModel.isScanning ? .blue : .orange) + .cornerRadius(10) + } + } +} + +// MARK: - ScanButton_Previews + +struct ScanButton_Previews: PreviewProvider { + // static let bleManager = BLEManager.live() + static var previews: some View { + ScanButton() + .environmentObject(RobotListViewModel.mock()) + } +} diff --git a/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/SendDataButton.swift b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/SendDataButton.swift new file mode 100644 index 0000000000..bf968001d5 --- /dev/null +++ b/Modules/BLEKit/Examples/BLEKitExample/Sources/Views/SendDataButton.swift @@ -0,0 +1,72 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import SwiftUI + +// MARK: - SendDataButton + +struct SendDataButton: View { + // MARK: Internal + + // MARK: - Environment properties + + @EnvironmentObject var robotListViewModel: RobotListViewModel + + // MARK: - Public views + + var body: some View { + Button { + self.sendData() + } label: { + self.labelView + } + .opacity((self.robotListViewModel.connectedRobotPeripheral == nil) ? 0.5 : 1.0) + .disabled(self.robotListViewModel.connectedRobotPeripheral == nil) + } + + // MARK: Private + + // MARK: - Private views + + private var labelView: some View { + HStack { + Text( + (self.robotListViewModel.connectedRobotPeripheral == nil) + ? "Select a robot" + : "Send data to \(self.robotListViewModel.connectedRobotPeripheral?.peripheral.name ?? "nil")") + Image(systemName: "paperplane.circle.fill") + .font(.title) + .foregroundColor(.teal) + } + .font(.headline) + .monospacedDigit() + .foregroundColor(.teal) + .frame(height: 50) + .frame(maxWidth: .infinity) + .background(.white) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(.teal, lineWidth: 2) + ) + } + + // MARK: - Private functions + + private func sendData() { + print("Send data") + self.robotListViewModel.connectedRobotPeripheral? + .sendCommand(Data([0x2A, 0x2A, 0x2A, 0x2A, 0x01, 0x50, 0x51, 0x51])) + } +} + +// MARK: - SendDataButton_Previews + +struct SendDataButton_Previews: PreviewProvider { + static var previews: some View { + SendDataButton() + .environmentObject(RobotListViewModel.mock()) + } +} diff --git a/Modules/BLEKit/Project.swift b/Modules/BLEKit/Project.swift new file mode 100644 index 0000000000..0246760d9b --- /dev/null +++ b/Modules/BLEKit/Project.swift @@ -0,0 +1,20 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.module( + name: "BLEKit", + examples: [ + ModuleExample( + name: "BLEKitExample" + ), + ], + dependencies: [ + .external(name: "CombineCoreBluetooth"), + ] +) diff --git a/Modules/BLEKit/Resources/Assets.xcassets/Contents.json b/Modules/BLEKit/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/BLEKit/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/BLEKit/Sources/BLEManager.swift b/Modules/BLEKit/Sources/BLEManager.swift new file mode 100644 index 0000000000..643b69facd --- /dev/null +++ b/Modules/BLEKit/Sources/BLEManager.swift @@ -0,0 +1,131 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import CombineCoreBluetooth + +public class BLEManager { + // MARK: Lifecycle + + // MARK: - Public functions + + public init(centralManager: CentralManager) { + self.centralManager = centralManager + + self.subscribeToDidDisconnect() + self.subscribeToDidConnect() + } + + // MARK: Public + + #if targetEnvironment(simulator) + public static var shared: BLEManager = .init(centralManager: .live()) + #else + public static var shared: BLEManager = .init( + centralManager: .live( + ManagerCreationOptions(showPowerAlert: true, restoreIdentifier: "io.leka.module.BLEKit.Manager.live")) + ) + #endif + + // MARK: - @Published variables + + public let isScanning = CurrentValueSubject(false) + public let didConnect = PassthroughSubject() + public let didDisconnect = PassthroughSubject() + + public var isConnected: Bool { + self.connectedRobotPeripheral != nil ? true : false + } + + public static func live() -> BLEManager { + BLEManager(centralManager: CentralManager.live()) + } + + public func scanForRobots() -> AnyPublisher<[RobotDiscoveryModel], Error> { + self.centralManager.scanForPeripherals(withServices: [BLESpecs.AdvertisingData.service]) + .handleEvents( + receiveSubscription: { _ in + if self.centralManager.state == .poweredOn { + self.isScanning.send(true) + } else { + self.isScanning.send(false) + } + }, + receiveCancel: { + self.isScanning.send(false) + } + ) + .tryScan( + [], + { list, discovery -> [PeripheralDiscovery] in + guard let index = list.firstIndex(where: { + $0.id == discovery.id + }) + else { + return list + [discovery] + } + var newList = list + newList[index] = discovery + return newList + } + ) + .compactMap { peripheralDiscoveries in + peripheralDiscoveries.compactMap { peripheralDiscovery in + guard let robotAdvertisingData = RobotAdvertisingData( + advertisementData: peripheralDiscovery.advertisementData), + let rssi = peripheralDiscovery.rssi + else { + return nil + } + + let robotPeripheral = RobotPeripheral(peripheral: peripheralDiscovery.peripheral) + + return RobotDiscoveryModel( + robotPeripheral: robotPeripheral, advertisingData: robotAdvertisingData, rssi: rssi + ) + } + } + .eraseToAnyPublisher() + } + + public func connect(_ discovery: RobotDiscoveryModel) -> AnyPublisher { + self.centralManager.connect(discovery.robotPeripheral.peripheral) + .compactMap { peripheral in + self.connectedRobotPeripheral = RobotPeripheral(peripheral: peripheral) + return self.connectedRobotPeripheral + } + .eraseToAnyPublisher() + } + + public func disconnect() { + guard let connectedPeripheral = connectedRobotPeripheral?.peripheral else { return } + self.centralManager.cancelPeripheralConnection(connectedPeripheral) + } + + // MARK: Private + + // MARK: - Private variables + + private var centralManager: CentralManager + private var connectedRobotPeripheral: RobotPeripheral? + + private var cancellables: Set = [] + + // MARK: - Private functions + + private func subscribeToDidConnect() { + self.centralManager.didConnectPeripheral + .sink { peripheral in + self.didConnect.send(RobotPeripheral(peripheral: peripheral)) + } + .store(in: &self.cancellables) + } + + private func subscribeToDidDisconnect() { + self.centralManager.didDisconnectPeripheral + .sink { _ in + self.didDisconnect.send() + } + .store(in: &self.cancellables) + } +} diff --git a/Modules/BLEKit/Sources/BLESpecs.swift b/Modules/BLEKit/Sources/BLESpecs.swift new file mode 100644 index 0000000000..bdef0f0f75 --- /dev/null +++ b/Modules/BLEKit/Sources/BLESpecs.swift @@ -0,0 +1,95 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import CoreBluetooth +import Foundation + +// swiftlint:disable nesting + +public enum BLESpecs { + public enum AdvertisingData { + public static let service = CBUUID(string: "0xDFB0") + } + + public enum DeviceInformation { + public enum Characteristics { + public static let manufacturer = CBUUID(string: "0x2A29") + public static let modelNumber = CBUUID(string: "0x2A24") + public static let serialNumber = CBUUID(string: "0x2A25") + public static let osVersion = CBUUID(string: "0x2A26") + } + + public static let service = CBUUID(string: "0x180A") + } + + public enum Battery { + public enum Characteristics { + public static let level = CBUUID(string: "0x2A19") + } + + public static let service = CBUUID(string: "0x180F") + } + + public enum Monitoring { + public enum Characteristics { + public static let chargingStatus = CBUUID(string: "0x6783") + public static let screensaverEnable = CBUUID(string: "0x8369") + public static let softReboot = CBUUID(string: "0x8382") + public static let hardReboot = CBUUID(string: "0x7282") + } + + public static let service = CBUUID(string: "0x7779") + } + + public enum Config { + public enum Characteristics { + public static let robotName = CBUUID(string: "8278") + } + + public static let service = CBUUID(string: "0x6770") + } + + public enum MagicCard { + public enum Characteristics { + public static let id = CBUUID(data: Data("ID".utf8) + Data([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])) + public static let language = CBUUID(data: Data("Language".utf8) + Data([0, 0, 0, 0, 0, 0, 0, 0])) + } + + public static let service = CBUUID(data: Data("Magic Card".utf8 + Data([0, 0, 0, 0, 0, 0]))) + } + + public enum FileExchange { + public enum Characteristics { + public static let setState = CBUUID(string: "0x8383") + public static let filePath = CBUUID(string: "0x7080") + public static let clearFile = CBUUID(string: "0x6770") + public static let fileReceptionBuffer = CBUUID(string: "0x8283") + public static let fileSHA256 = CBUUID(string: "0x7083") + } + + public static let service = CBUUID(string: "0x8270") + } + + public enum FirmwareUpdate { + public enum Characteristics { + public static let requestUpdate = CBUUID(string: "0x8285") + public static let requestFactoryReset = CBUUID(string: "0x8270") + public static let versionMajor = CBUUID(string: "0x7765") + public static let versionMinor = CBUUID(string: "0x7773") + public static let versionRevision = CBUUID(string: "0x8269") + } + + public static let service = CBUUID(string: "0x7085") + } + + public enum Commands { + public enum Characteristics { + public static let tx = CBUUID(string: "0xDFB1") + } + + public static let service = CBUUID(string: "0xDFB0") + } +} + +// swiftlint:enable nesting diff --git a/Modules/BLEKit/Sources/Extensions/String+Extension.swift b/Modules/BLEKit/Sources/Extensions/String+Extension.swift new file mode 100644 index 0000000000..aa52ebc72a --- /dev/null +++ b/Modules/BLEKit/Sources/Extensions/String+Extension.swift @@ -0,0 +1,11 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +public extension String { + var nilWhenEmpty: String? { + isEmpty ? nil : self + } +} diff --git a/Modules/BLEKit/Sources/Extensions/UInt16+Extension.swift b/Modules/BLEKit/Sources/Extensions/UInt16+Extension.swift new file mode 100644 index 0000000000..74d79ab4cb --- /dev/null +++ b/Modules/BLEKit/Sources/Extensions/UInt16+Extension.swift @@ -0,0 +1,19 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +public extension UInt16 { + var highByte: UInt8 { + UInt8(self >> 8) + } + + var lowByte: UInt8 { + UInt8(self & 0xFF) + } + + var data: Data { + Data([self.highByte, self.lowByte]) + } +} diff --git a/Modules/BLEKit/Sources/Mocks/RobotDiscoveryModel+Mocks.swift b/Modules/BLEKit/Sources/Mocks/RobotDiscoveryModel+Mocks.swift new file mode 100644 index 0000000000..b9af64784b --- /dev/null +++ b/Modules/BLEKit/Sources/Mocks/RobotDiscoveryModel+Mocks.swift @@ -0,0 +1,11 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +public extension RobotDiscoveryModel { + static func mock() -> RobotDiscoveryModel { + RobotDiscoveryModel( + name: "Leka Mock", isCharging: Bool.random(), battery: Int.random(in: 1..<100), osVersion: "1.2.3" + ) + } +} diff --git a/Modules/BLEKit/Sources/Models/AdvertisingServiceData.swift b/Modules/BLEKit/Sources/Models/AdvertisingServiceData.swift new file mode 100644 index 0000000000..eeb08d0fe5 --- /dev/null +++ b/Modules/BLEKit/Sources/Models/AdvertisingServiceData.swift @@ -0,0 +1,56 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +// MARK: - AdvertisingServiceData + +struct AdvertisingServiceData { + // MARK: Lifecycle + + init(data: Data) { + self.battery = getBattery(data: data) + self.isCharging = getChargingState(data: data) + self.osVersion = getOsVersion(data: data) + } + + // MARK: Internal + + let battery: Int + let isCharging: Bool + let osVersion: String? +} + +// MARK: - AdvertisingServiceDataIndex + +private enum AdvertisingServiceDataIndex { + static let battery = 0 + static let isCharging = 1 + static let osVersionMajor = 2 + static let osVersionMinor = 3 + static let osVersionRevisionHighByte = 4 + static let osVersionRevisionLowByte = 5 +} + +private func getBattery(data: Data) -> Int { + Int(data[AdvertisingServiceDataIndex.battery]) +} + +private func getChargingState(data: Data) -> Bool { + data[AdvertisingServiceDataIndex.isCharging] == 0x01 +} + +private func getOsVersion(data: Data) -> String? { + guard data.count == 6 else { + return nil + } + + let major = data[AdvertisingServiceDataIndex.osVersionMajor] + let minor = data[AdvertisingServiceDataIndex.osVersionMinor] + let revision = + Int(data[AdvertisingServiceDataIndex.osVersionRevisionHighByte]) << 8 + + Int(data[AdvertisingServiceDataIndex.osVersionRevisionLowByte]) + + return "\(major).\(minor).\(revision)" +} diff --git a/Modules/BLEKit/Sources/Models/CharacteristicModelNotifying.swift b/Modules/BLEKit/Sources/Models/CharacteristicModelNotifying.swift new file mode 100644 index 0000000000..6b4499b1e7 --- /dev/null +++ b/Modules/BLEKit/Sources/Models/CharacteristicModelNotifying.swift @@ -0,0 +1,62 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import CombineCoreBluetooth + +// MARK: - CharacteristicModelNotifying + +public struct CharacteristicModelNotifying { + // MARK: Lifecycle + + public init( + characteristicUUID: CBUUID, serviceUUID: CBUUID, cbCharacteristic: CBCharacteristic? = nil, + onNotification: Callback? = nil + ) { + self.characteristicUUID = characteristicUUID + self.serviceUUID = serviceUUID + self.cbCharacteristic = cbCharacteristic + self.onNotification = onNotification + } + + // MARK: Public + + public typealias Callback = (_ data: Data?) -> Void + + public let characteristicUUID: CBUUID + public let serviceUUID: CBUUID + public let cbCharacteristic: CBCharacteristic? + public let onNotification: Callback? +} + +// MARK: Hashable + +extension CharacteristicModelNotifying: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.characteristicUUID) + hasher.combine(self.serviceUUID) + } + + public static func == (lhs: CharacteristicModelNotifying, rhs: CharacteristicModelNotifying) -> Bool { + lhs.serviceUUID == rhs.serviceUUID && lhs.characteristicUUID == rhs.characteristicUUID + } +} + +public let kDefaultNotifyingCharacteristics: Set = [ + CharacteristicModelNotifying( + characteristicUUID: BLESpecs.Battery.Characteristics.level, + serviceUUID: BLESpecs.Battery.service + ), + CharacteristicModelNotifying( + characteristicUUID: BLESpecs.Monitoring.Characteristics.chargingStatus, + serviceUUID: BLESpecs.Monitoring.service + ), + CharacteristicModelNotifying( + characteristicUUID: BLESpecs.MagicCard.Characteristics.id, + serviceUUID: BLESpecs.MagicCard.service + ), + CharacteristicModelNotifying( + characteristicUUID: BLESpecs.MagicCard.Characteristics.language, + serviceUUID: BLESpecs.MagicCard.service + ), +] diff --git a/Modules/BLEKit/Sources/Models/CharacteristicModelReadOnly.swift b/Modules/BLEKit/Sources/Models/CharacteristicModelReadOnly.swift new file mode 100644 index 0000000000..2030ba6574 --- /dev/null +++ b/Modules/BLEKit/Sources/Models/CharacteristicModelReadOnly.swift @@ -0,0 +1,57 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import CombineCoreBluetooth + +// MARK: - CharacteristicModelReadOnly + +public struct CharacteristicModelReadOnly { + // MARK: Lifecycle + + public init(characteristicUUID: CBUUID, serviceUUID: CBUUID, onRead: Callback? = nil) { + self.characteristicUUID = characteristicUUID + self.serviceUUID = serviceUUID + self.onRead = onRead + } + + // MARK: Public + + public typealias Callback = (_ data: Data?) -> Void + + public let characteristicUUID: CBUUID + public let serviceUUID: CBUUID + public let onRead: Callback? +} + +// MARK: Hashable + +extension CharacteristicModelReadOnly: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.characteristicUUID) + hasher.combine(self.serviceUUID) + } + + public static func == (lhs: CharacteristicModelReadOnly, rhs: CharacteristicModelReadOnly) -> Bool { + lhs.serviceUUID == rhs.serviceUUID && lhs.characteristicUUID == rhs.characteristicUUID + } +} + +public let kDefaultReadOnlyCharacteristics: Set = [ + CharacteristicModelReadOnly( + characteristicUUID: BLESpecs.DeviceInformation.Characteristics.manufacturer, + serviceUUID: BLESpecs.DeviceInformation.service + ), + CharacteristicModelReadOnly( + characteristicUUID: BLESpecs.DeviceInformation.Characteristics.modelNumber, + serviceUUID: BLESpecs.DeviceInformation.service + ), + CharacteristicModelReadOnly( + characteristicUUID: BLESpecs.DeviceInformation.Characteristics.serialNumber, + serviceUUID: BLESpecs.DeviceInformation.service + ), + CharacteristicModelReadOnly( + characteristicUUID: BLESpecs.DeviceInformation.Characteristics.osVersion, + serviceUUID: BLESpecs.DeviceInformation.service + ), +] diff --git a/Modules/BLEKit/Sources/Models/CharacteristicModelWriteOnly.swift b/Modules/BLEKit/Sources/Models/CharacteristicModelWriteOnly.swift new file mode 100644 index 0000000000..45819f879f --- /dev/null +++ b/Modules/BLEKit/Sources/Models/CharacteristicModelWriteOnly.swift @@ -0,0 +1,23 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import CombineCoreBluetooth + +public struct CharacteristicModelWriteOnly { + // MARK: Lifecycle + + public init(characteristicUUID: CBUUID, serviceUUID: CBUUID, onWrite: Callback? = nil) { + self.characteristicUUID = characteristicUUID + self.serviceUUID = serviceUUID + self.onWrite = onWrite + } + + // MARK: Public + + public typealias Callback = () -> Void + + public let characteristicUUID: CBUUID + public let serviceUUID: CBUUID + public let onWrite: Callback? +} diff --git a/Modules/BLEKit/Sources/Models/RobotAdvertisingData.swift b/Modules/BLEKit/Sources/Models/RobotAdvertisingData.swift new file mode 100644 index 0000000000..c1f158aee9 --- /dev/null +++ b/Modules/BLEKit/Sources/Models/RobotAdvertisingData.swift @@ -0,0 +1,38 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import CombineCoreBluetooth + +public struct RobotAdvertisingData { + // MARK: Lifecycle + + // MARK: - Public functions + + public init(name: String?, serviceData: Data) { + let serviceData = AdvertisingServiceData(data: serviceData) + self.name = name?.nilWhenEmpty ?? "⚠️ NO NAME" + self.battery = serviceData.battery + self.isCharging = serviceData.isCharging + self.osVersion = serviceData.osVersion + } + + public init?(advertisementData: AdvertisementData) { + guard let rawServiceData = advertisementData.serviceData, + let robotServiceData = rawServiceData[BLESpecs.AdvertisingData.service] + else { + return nil + } + + self.init(name: advertisementData.localName, serviceData: robotServiceData) + } + + // MARK: Public + + // MARK: - Public variables + + public let name: String + public let battery: Int + public let isCharging: Bool + public let osVersion: String? +} diff --git a/Modules/BLEKit/Sources/Models/RobotDiscoveryModel.swift b/Modules/BLEKit/Sources/Models/RobotDiscoveryModel.swift new file mode 100644 index 0000000000..462c08ba17 --- /dev/null +++ b/Modules/BLEKit/Sources/Models/RobotDiscoveryModel.swift @@ -0,0 +1,70 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import CombineCoreBluetooth + +// MARK: - RobotDiscoveryModel + +public struct RobotDiscoveryModel: Identifiable { + // MARK: Lifecycle + + // MARK: - Public functions + + public init(name: String, isCharging: Bool, battery: Int, osVersion: String) { + self.robotPeripheral = nil + self.rssi = nil + self.id = UUID() + self.name = name + self.isCharging = isCharging + self.battery = battery + self.osVersion = osVersion + } + + public init(robotPeripheral: RobotPeripheral, advertisingData: RobotAdvertisingData, rssi: Double?) { + self.robotPeripheral = robotPeripheral + self.rssi = rssi + self.id = robotPeripheral.peripheral.id + self.name = advertisingData.name + self.isCharging = advertisingData.isCharging + self.battery = advertisingData.battery + self.osVersion = computeVersion(version: advertisingData.osVersion, name: advertisingData.name) + } + + // MARK: Public + + // MARK: - Public variables + + public let robotPeripheral: RobotPeripheral! + public let rssi: Double? + + public let id: UUID + public let name: String + public let isCharging: Bool + public let battery: Int + public let osVersion: String +} + +// MARK: Equatable + +extension RobotDiscoveryModel: Equatable { + public static func == (lhs: RobotDiscoveryModel, rhs: RobotDiscoveryModel) -> Bool { + lhs.id == rhs.id + } +} + +private func computeVersion(version: String?, name: String) -> String { + if let version { + return "\(version)" + } + + if name == "Leka" { + return "1.0.0" + } else if name.contains("LK-"), name.contains("xx") { + return "1.1.0" + } else if name.contains("LK-") { + return "1.2.0" + } else { + return "(n/a)" + } +} diff --git a/Modules/BLEKit/Sources/Models/RobotPeripheral.swift b/Modules/BLEKit/Sources/Models/RobotPeripheral.swift new file mode 100644 index 0000000000..a156d7e496 --- /dev/null +++ b/Modules/BLEKit/Sources/Models/RobotPeripheral.swift @@ -0,0 +1,149 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import CombineCoreBluetooth + +public class RobotPeripheral: Equatable { + // MARK: Lifecycle + + // MARK: - Public functions + + public init(peripheral: Peripheral) { + self.peripheral = peripheral + } + + // MARK: Public + + // MARK: - Public variables + + // TODO(@ladislas): should they be published? maybe, need to investigate + public var peripheral: Peripheral + public var notifyingCharacteristics: Set = [] + public var readOnlyCharacteristics: Set = [] + + public static func == (lhs: RobotPeripheral, rhs: RobotPeripheral) -> Bool { + lhs.peripheral.id == rhs.peripheral.id + } + + public func discoverAndListenForUpdates() { + for char in self.notifyingCharacteristics { + self.peripheral + .discoverCharacteristic( + withUUID: char.characteristicUUID, inServiceWithUUID: char.serviceUUID + ) + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { _ in + // nothing to do + }, + receiveValue: { characteristic in + self.peripheral.setNotifyValue(true, for: characteristic) + .assertNoFailure() + .sink { + let newCharacteristic = CharacteristicModelNotifying( + characteristicUUID: char.characteristicUUID, + serviceUUID: char.serviceUUID, + cbCharacteristic: characteristic, + onNotification: char.onNotification + ) + + self.notifyingCharacteristics.remove(char) + self.notifyingCharacteristics.insert(newCharacteristic) + + self.listenForUpdates(on: newCharacteristic) + } + .store(in: &self.cancellables) + } + ) + .store(in: &self.cancellables) + } + } + + public func readReadOnlyCharacteristics() { + for characteristic in self.readOnlyCharacteristics { + self.peripheral.readValue( + forCharacteristic: characteristic.characteristicUUID, + inService: characteristic.serviceUUID + ) + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { _ in + // nothing to do + }, + receiveValue: { data in + guard let data else { return } + characteristic.onRead?(data) + } + ) + .store(in: &self.cancellables) + } + } + + public func sendCommand(_ data: Data) { + self.peripheral.writeValue( + data, + writeType: .withoutResponse, + forCharacteristic: BLESpecs.Commands.Characteristics.tx, + inService: BLESpecs.Commands.service + ) + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { _ in + // nothing to do + }, + receiveValue: { _ in + // nothing to do + } + ) + .store(in: &self.cancellables) + } + + public func send(_ data: Data, forCharacteristic characteristic: CharacteristicModelWriteOnly) { + self.peripheral.writeValue( + data, + writeType: .withResponse, + forCharacteristic: characteristic.characteristicUUID, + inService: characteristic.serviceUUID + ) + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { completion in + switch completion { + case .finished: + characteristic.onWrite?() + case let .failure(error): + print("💥 ERROR: \(error)") + } + }, + receiveValue: { _ in + // nothing to do + } + ) + .store(in: &self.cancellables) + } + + // MARK: Private + + // MARK: - Private variables + + private var cancellables: Set = [] + + // MARK: - Private functions + + private func listenForUpdates(on characteristic: CharacteristicModelNotifying) { + self.peripheral.listenForUpdates(on: characteristic.cbCharacteristic!) + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { _ in + // nothing to do + }, + receiveValue: { data in + if let data { + characteristic.onNotification?(data) + } + } + ) + .store(in: &self.cancellables) + } +} diff --git a/Modules/BLEKit/Tests/AdvertisingServiceData_Tests.swift b/Modules/BLEKit/Tests/AdvertisingServiceData_Tests.swift new file mode 100644 index 0000000000..7cc07d5115 --- /dev/null +++ b/Modules/BLEKit/Tests/AdvertisingServiceData_Tests.swift @@ -0,0 +1,159 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import XCTest + +@testable import BLEKit + +// MARK: - AdvertisingServiceData_Tests_BatteryLevel + +final class AdvertisingServiceData_Tests_BatteryLevel: XCTestCase { + func test_shouldReturnBatteryLevel_equals0() { + // Given + let data = Data([0, 0, 0, 0, 0, 0]) + + // When + let serviceData = AdvertisingServiceData(data: data) + + // Then + let expected = 0 + let actual = serviceData.battery + + XCTAssertEqual(expected, actual) + } + + func test_shouldReturnBatteryLevel_equals50() { + // Given + let data = Data([50, 0, 0, 0, 0, 0]) + + // When + let serviceData = AdvertisingServiceData(data: data) + + // Then + let expected = 50 + let actual = serviceData.battery + + XCTAssertEqual(expected, actual) + } + + func test_shouldReturnBatteryLevel_equals100() { + // Given + let data = Data([100, 0, 0, 0, 0, 0]) + + // When + let serviceData = AdvertisingServiceData(data: data) + + // Then + let expected = 100 + let actual = serviceData.battery + + XCTAssertEqual(expected, actual) + } +} + +// MARK: - AdvertisingServiceData_Tests_ChargingStatus + +final class AdvertisingServiceData_Tests_ChargingStatus: XCTestCase { + func test_shouldReturnChargingStatus_isCharging() { + // Given + let data = Data([0, 1, 0, 0, 0, 0]) + + // When + let serviceData = AdvertisingServiceData(data: data) + + // Then + let expected = true + let actual = serviceData.isCharging + + XCTAssertEqual(expected, actual) + } + + func test_shouldReturnChargingStatus_isNotCharging() { + // Given + let data = Data([0, 0, 0, 0, 0, 0]) + + // When + let serviceData = AdvertisingServiceData(data: data) + + // Then + let expected = false + let actual = serviceData.isCharging + + XCTAssertEqual(expected, actual) + } +} + +// MARK: - AdvertisingServiceData_Tests_OsVersion + +final class AdvertisingServiceData_Tests_OsVersion: XCTestCase { + func test_shouldReturnOsVersion_0_0_0() { + // Given + let data = Data([0, 0, 0, 0, 0, 0]) + + // When + let serviceData = AdvertisingServiceData(data: data) + + // Then + let expected = "0.0.0" + let actual = serviceData.osVersion + + XCTAssertEqual(expected, actual) + } + + func test_shouldReturnOsVersion_1_0_0() { + // Given + let data = Data([0, 0, 1, 0, 0, 0]) + + // When + let serviceData = AdvertisingServiceData(data: data) + + // Then + let expected = "1.0.0" + let actual = serviceData.osVersion + + XCTAssertEqual(expected, actual) + } + + func test_shouldReturnOsVersion_1_2_0() { + // Given + let data = Data([0, 0, 1, 2, 0, 0]) + + // When + let serviceData = AdvertisingServiceData(data: data) + + // Then + let expected = "1.2.0" + let actual = serviceData.osVersion + + XCTAssertEqual(expected, actual) + } + + func test_shouldReturnOsVersion_1_2_3() { + // Given + let data = Data([0, 0, 1, 2, 0, 3]) + + // When + let serviceData = AdvertisingServiceData(data: data) + + // Then + let expected = "1.2.3" + let actual = serviceData.osVersion + + XCTAssertEqual(expected, actual) + } + + func test_shouldReturnOsVersion_1_2_300() { + // Given + let data = Data([0, 0, 1, 2, 0x01, 0x2C]) + + // When + let serviceData = AdvertisingServiceData(data: data) + + // Then + let expected = "1.2.300" + let actual = serviceData.osVersion + + XCTAssertEqual(expected, actual) + } +} diff --git a/Modules/BLEKit/Tests/BLEKit_Tests.swift b/Modules/BLEKit/Tests/BLEKit_Tests.swift new file mode 100644 index 0000000000..82c4024671 --- /dev/null +++ b/Modules/BLEKit/Tests/BLEKit_Tests.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +final class BLEKit_Tests: XCTestCase { + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/Modules/BLEKit/Tests/RobotAdvertisingData_Tests.swift b/Modules/BLEKit/Tests/RobotAdvertisingData_Tests.swift new file mode 100644 index 0000000000..e573c61663 --- /dev/null +++ b/Modules/BLEKit/Tests/RobotAdvertisingData_Tests.swift @@ -0,0 +1,45 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import XCTest + +@testable import BLEKit + +final class RobotAdvertisingData_Tests: XCTestCase { + func test_shouldReturnNameWhenNameIsNotNil() { + // Given + + // When + let advertisingData = RobotAdvertisingData(name: "Leka Robot Name", serviceData: Data([0, 0, 0, 0, 0, 0])) + + // Then + let expected = "Leka Robot Name" + let actual = advertisingData.name + XCTAssertEqual(expected, actual) + } + + func test_shouldReturnNoNameWhenNameIsNil() { + // Given + + // When + let advertisingData = RobotAdvertisingData(name: nil, serviceData: Data([0, 0, 0, 0, 0, 0])) + + // Then + let expected = "⚠️ NO NAME" + let actual = advertisingData.name + XCTAssertEqual(expected, actual) + } + + func test_shouldReturnNoNameWhenNameIsEmpty() { + // Given + + // When + let advertisingData = RobotAdvertisingData(name: "", serviceData: Data([0, 0, 0, 0, 0, 0])) + + // Then + let expected = "⚠️ NO NAME" + let actual = advertisingData.name + XCTAssertEqual(expected, actual) + } +} diff --git a/Modules/BLEKit/Tests/UInt16+Extension_Tests.swift b/Modules/BLEKit/Tests/UInt16+Extension_Tests.swift new file mode 100644 index 0000000000..5100813149 --- /dev/null +++ b/Modules/BLEKit/Tests/UInt16+Extension_Tests.swift @@ -0,0 +1,72 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import XCTest + +@testable import BLEKit + +// MARK: - UInt16_Extension_Tests_highByte_lowByte + +final class UInt16_Extension_Tests_highByte_lowByte: XCTestCase { + func test_shouldReturnHighByte() { + // Given + let value: UInt16 = 0x4200 + + // When + + // Then + let expected: UInt8 = 0x42 + let actual = value.highByte + + XCTAssertEqual(expected, actual) + } + + func test_shouldReturnLowByte() { + // Given + let value: UInt16 = 0x0042 + + // When + + // Then + let expected: UInt8 = 0x42 + let actual = value.lowByte + + XCTAssertEqual(expected, actual) + } + + func test_shouldReturnHighByteAndLowByte() { + // Given + let value: UInt16 = 0xBEEF + + // When + + // Then + let expectedHigh: UInt8 = 0xBE + let actualHigh = value.highByte + + XCTAssertEqual(expectedHigh, actualHigh) + + let expectedLow: UInt8 = 0xEF + let actualLow = value.lowByte + + XCTAssertEqual(expectedLow, actualLow) + } +} + +// MARK: - UInt16_Extension_Tests_Data + +final class UInt16_Extension_Tests_Data: XCTestCase { + func test_shouldReturnData() { + // Given + let value: UInt16 = 0xBEEF + + // When + + // Then + let expected = Data([0xBE, 0xEF]) + let actual = value.data + + XCTAssertEqual(expected, actual) + } +} diff --git a/Modules/ContentKit/Examples/ContentKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Modules/ContentKit/Examples/ContentKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Modules/ContentKit/Examples/ContentKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/ContentKit/Examples/ContentKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Modules/ContentKit/Examples/ContentKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Modules/ContentKit/Examples/ContentKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/ContentKit/Examples/ContentKitExample/Resources/Assets.xcassets/Contents.json b/Modules/ContentKit/Examples/ContentKitExample/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/ContentKit/Examples/ContentKitExample/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/ContentKit/Examples/ContentKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Modules/ContentKit/Examples/ContentKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/ContentKit/Examples/ContentKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/ContentKit/Examples/ContentKitExample/Resources/activity.yml b/Modules/ContentKit/Examples/ContentKitExample/Resources/activity.yml new file mode 100644 index 0000000000..2de4d34478 --- /dev/null +++ b/Modules/ContentKit/Examples/ContentKitExample/Resources/activity.yml @@ -0,0 +1,144 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +uuid: 5A670B59EB214A6AA11AB39D64D63990 +name: contentkit-specs-jtd-tests-activity_good + +status: published + +authors: + - leka + - aurore_kiesler + - julie_tuil + +skills: + - spatial_understanding + - recognition/animals + - communication/non_verbal_communication/gestures + +tags: + - tag_one + - tag_two + - tag_three + +locales: + - en_US + - fr_FR + +hmi: + - robot + - magic_cards + - tablet_robot + +l10n: + - locale: fr_FR + details: + icon: name_of_the_activity-icon-fr_FR.svg + + title: Le titre/nom de l'activité + subtitle: Le sous-titre de l'activité + + description: | + Courte description de l'activité sur plusieurs lignes + Si besoin! + Et en markdown **simple** seulement + + instructions: | + ## Longues instructions en markdown plus complexe si on veut + + Lorem markdownum recepta avidum, missa de quam patientia, antris: cum defuit, + Titan repetemus nomine, ignare. Quod ad aura, et non quod vidisse utque ulla: + + - Pro inposuit tibi orsa tum artes ferox + - Acmon plausu qua agrestum situs virgo in + - Vacuus a pendens rostro non si pharetrae + - Haeremusque quos auxiliaris coniunx + - Repulsa impediunt munera teneri fallebat + - Bracchia frustra telo Iovis faucibus casus + + - locale: en_US + details: + icon: name_of_the_activity-icon-en_US.svg + + title: The title/name of the activity + subtitle: The subtitle of the activity + + description: | + Short description of the activity on several lines + If needed! + And in **simple** markdown only + + instructions: | + ## Long instructions in more complex markdown if we want + + Lorem markdownum recepta avidum, missa de quam patientia, antris: cum defuit, + Titan repetemus nomine, ignare. Quod ad aura, et non quod vidisse utque ulla: + + - Pro inposuit tibi orsa tum artes ferox + - Acmon plausu qua agrestum situs virgo in + - Vacuus a pendens rostro non si pharetrae + - Haeremusque quos auxiliaris coniunx + - Repulsa impediunt munera teneri fallebat + - Bracchia frustra telo Iovis faucibus casus + +gameengine: + shuffle_exercises: true + shuffle_groups: true + +exercise_groups: + - group: + - instructions: + - locale: fr_FR + value: Place les instruments dans les bonnes familles d'instruments + - locale: en_US + value: Place the instruments in the right instrument families + interface: dragAndDropIntoZones + gameplay: findTheRightAnswers + payload: "placeholder payload" + + - instructions: + - locale: fr_FR + value: Place les instruments dans les bonnes familles d'instruments + - locale: en_US + value: Place the instruments in the right instrument families + interface: dragAndDropIntoZones + gameplay: findTheRightAnswers + payload: "placeholder payload" + + - instructions: + - locale: fr_FR + value: Place les instruments dans les bonnes familles d'instruments + - locale: en_US + value: Place the instruments in the right instrument families + interface: dragAndDropIntoZones + gameplay: findTheRightAnswers + payload: "placeholder payload" + + - group: + - instructions: + - locale: fr_FR + value: Place les instruments dans les bonnes familles d'instruments + - locale: en_US + value: Place the instruments in the right instrument families + interface: dragAndDropIntoZones + gameplay: findTheRightAnswers + payload: "placeholder payload" + + - instructions: + - locale: fr_FR + value: Place les instruments dans les bonnes familles d'instruments + - locale: en_US + value: Place the instruments in the right instrument families + interface: dragAndDropIntoZones + gameplay: findTheRightAnswers + payload: "placeholder payload" + + - instructions: + - locale: fr_FR + value: Place les instruments dans les bonnes familles d'instruments + - locale: en_US + value: Place the instruments in the right instrument families + interface: dragAndDropIntoZones + gameplay: findTheRightAnswers + payload: "placeholder payload" diff --git a/Modules/ContentKit/Examples/ContentKitExample/Sources/ActivityDetailsView.swift b/Modules/ContentKit/Examples/ContentKitExample/Sources/ActivityDetailsView.swift new file mode 100644 index 0000000000..5d90e9e936 --- /dev/null +++ b/Modules/ContentKit/Examples/ContentKitExample/Sources/ActivityDetailsView.swift @@ -0,0 +1,158 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import LocalizationKit +import MarkdownUI +import SwiftUI + +// MARK: - RowView + +struct RowView: View { + // MARK: Lifecycle + + init(label: String) where T == String { + self.label = label + self.value = "" + } + + init(label: String, value: T?) { + self.label = label + self.value = value + } + + // MARK: Internal + + let label: String + let value: T? + + var body: some View { + HStack { + Text("**\(self.label)**") + Spacer() + Text(self.value ?? "") + .foregroundStyle(.secondary) + } + } +} + +// MARK: - ActivityDetailsView + +struct ActivityDetailsView: View { + // MARK: Internal + + let activity: Activity + + var body: some View { + List { + Section("Information") { + RowView(label: "UUID", value: self.activity.id) + RowView(label: "Name", value: self.activity.name) + RowView(label: "Created at", value: self.activity.createdAt.description) + RowView(label: "Last edited at", value: self.activity.lastEditedAt.description) + + RowView(label: "Status", value: self.activity.status == .published ? "published" : "draft") + + DisclosureGroup("**Authors**") { + ForEach(self.activity.authors, id: \.self) { author in + let author = Authors.hmi(id: author)! + HStack { + Text(author.name) + Button { + self.selectedAuthor = author + } label: { + Image(systemName: "info.circle") + } + } + } + } + .sheet(item: self.$selectedAuthor, onDismiss: { self.selectedAuthor = nil }, content: { author in + VStack(alignment: .leading) { + Text(author.name) + .font(.headline) + Text(author.description) + } + }) + + DisclosureGroup("**Available languages**") { + ForEach(self.activity.languages, id: \.self) { lang in + Text(lang.identifier) + } + } + + DisclosureGroup("**Skills**") { + ForEach(self.activity.skills, id: \.self) { skill in + let skill = Skills.skill(id: skill)! + HStack { + Text(skill.name) + Button { + self.selectedSkill = skill + } label: { + Image(systemName: "info.circle") + } + } + } + } + .sheet(item: self.$selectedSkill, onDismiss: { self.selectedSkill = nil }, content: { skill in + VStack(alignment: .leading) { + Text(skill.name) + .font(.headline) + Text(skill.description) + } + }) + + DisclosureGroup("**HMI**") { + ForEach(self.activity.hmi, id: \.self) { hmi in + let hmi = HMI.hmi(id: hmi)! + HStack { + Text(hmi.name) + Button { + self.selectedHMI = hmi + } label: { + Image(systemName: "info.circle") + } + } + } + } + .sheet(item: self.$selectedHMI, onDismiss: { self.selectedHMI = nil }, content: { hmi in + VStack(alignment: .leading) { + Text(hmi.name) + .font(.headline) + Text(hmi.description) + } + }) + + DisclosureGroup("**Tags**") { + ForEach(self.activity.tags, id: \.self) { skill in + Text(skill) + } + } + } + + Section("Details (in: \(l10n.language.identifier))") { + Text(self.activity.details.title) + .font(.title) + Text(self.activity.details.subtitle ?? "no subtitle") + .font(.title2) + Markdown(self.activity.details.description) + .markdownTheme(.gitHub) + Markdown(self.activity.details.instructions) + .markdownTheme(.gitHub) + } + } + .navigationTitle(self.activity.name) + } + + // MARK: Private + + @State private var selectedSkill: Skill? + @State private var selectedHMI: HMIDetails? + @State private var selectedAuthor: Author? +} + +#Preview { + NavigationStack { + ActivityDetailsView(activity: Activity.mock) + } +} diff --git a/Modules/ContentKit/Examples/ContentKitExample/Sources/ActivityListView.swift b/Modules/ContentKit/Examples/ContentKitExample/Sources/ActivityListView.swift new file mode 100644 index 0000000000..1050e1ffc6 --- /dev/null +++ b/Modules/ContentKit/Examples/ContentKitExample/Sources/ActivityListView.swift @@ -0,0 +1,59 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import MarkdownUI +import SwiftUI + +// MARK: - ActivityListView + +struct ActivityListView: View { + let activities: [Activity] = ContentKit.listSampleActivities() ?? [] + + var body: some View { + List { + ForEach(self.activities) { activity in + NavigationLink(destination: ActivityDetailsView(activity: activity)) { + Image(uiImage: activity.details.iconImage) + .resizable() + .scaledToFit() + .frame(width: 44, height: 44) + Text(activity.name) + } + } + } + .navigationTitle("Activities") + .onAppear { + let skills = Skills.list + for (index, skill) in skills.enumerated() { + print("skill \(index + 1)") + print("id: \(skill.id)") + print("name: \(skill.name)") + print("description: \(skill.description)") + } + + let hmis = HMI.list + for (index, hmi) in hmis.enumerated() { + print("hmi \(index + 1)") + print("id: \(hmi.id)") + print("name: \(hmi.name)") + print("description: \(hmi.description)") + } + + let authors = Authors.list + for (index, author) in authors.enumerated() { + print("author \(index + 1)") + print("id: \(author.id)") + print("name: \(author.name)") + print("description: \(author.description)") + } + } + } +} + +#Preview { + NavigationStack { + ActivityListView() + } +} diff --git a/Modules/ContentKit/Examples/ContentKitExample/Sources/MainApp.swift b/Modules/ContentKit/Examples/ContentKitExample/Sources/MainApp.swift new file mode 100644 index 0000000000..31c5ef3e0f --- /dev/null +++ b/Modules/ContentKit/Examples/ContentKitExample/Sources/MainApp.swift @@ -0,0 +1,17 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +@main +struct ContentKitExample: App { + var body: some Scene { + WindowGroup { + NavigationStack { + ActivityListView() + } + } + } +} diff --git a/Modules/ContentKit/Project.swift b/Modules/ContentKit/Project.swift new file mode 100644 index 0000000000..f17cfe012f --- /dev/null +++ b/Modules/ContentKit/Project.swift @@ -0,0 +1,26 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.module( + name: "ContentKit", + examples: [ + ModuleExample( + name: "ContentKitExample", + dependencies: [ + .external(name: "MarkdownUI"), + ] + ), + ], + dependencies: [ + .project(target: "LocalizationKit", path: Path("../../Modules/LocalizationKit")), + .project(target: "LogKit", path: Path("../../Modules/LogKit")), + .external(name: "Version"), + .external(name: "Yams"), + ] +) diff --git a/Modules/ContentKit/Resources/Assets.xcassets/Contents.json b/Modules/ContentKit/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/ContentKit/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/ContentKit/Resources/Content/activities/remotes/icons/remote_colored_arrows.activity.icon.png b/Modules/ContentKit/Resources/Content/activities/remotes/icons/remote_colored_arrows.activity.icon.png new file mode 100755 index 0000000000..952666bfe7 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/remotes/icons/remote_colored_arrows.activity.icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a148be3d7b706b8d7b628c912ed3d1bb00bf5d188b6ce6786cc89d80bddd9d1 +size 11581 diff --git a/Modules/ContentKit/Resources/Content/activities/remotes/icons/remote_standard.activity.icon.png b/Modules/ContentKit/Resources/Content/activities/remotes/icons/remote_standard.activity.icon.png new file mode 100755 index 0000000000..d4a3222f11 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/remotes/icons/remote_standard.activity.icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2f3b21eeb772cd510ff446dec5a0347d1522227ea5f8b57d27eac9cb9240113 +size 9579 diff --git a/Modules/ContentKit/Resources/Content/activities/remotes/remote_colored_arrows-D91BDA161F8E455CA8A71881F1D2E923.activity.yml b/Modules/ContentKit/Resources/Content/activities/remotes/remote_colored_arrows-D91BDA161F8E455CA8A71881F1D2E923.activity.yml new file mode 100644 index 0000000000..2d6da626df --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/remotes/remote_colored_arrows-D91BDA161F8E455CA8A71881F1D2E923.activity.yml @@ -0,0 +1,83 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 + +uuid: D91BDA161F8E455CA8A71881F1D2E923 +name: remote_colored_arrows + +created_at: "2024-03-04T18:07:12.518998" +last_edited_at: "2024-03-05T09:42:38.078478" +status: published + +authors: + - leka + +skills: + - sensory_interaction + - gross_motor_skills + +tags: + - robot_movements + - robot_colors + +hmi: + - tablet_robot + +types: + - one_on_one + - group + +locales: + - en_US + - fr_FR + +l10n: + - locale: fr_FR + details: + icon: remote_colored_arrows + + title: Commande Fléchée + subtitle: null + + short_description: Contrôle Leka avec la télécommande, fais le se déplacer en + changeant de couleur + + description: | + La commande Fléchée permet de téléguider le robot Leka. + + instructions: | + Appuie sur une des flèches pour téléguider Leka dans la direction de ton choix. + + - locale: en_US + details: + icon: remote_colored_arrows + + title: Remote Arrow + subtitle: null + + short_description: | + Control Leka with the remote control, make it move by changing color + + description: | + The Remote Arrow allows you to remotely guide the Leka robot. + + instructions: | + Press one of the arrows to remotely guide Leka in the direction of your choice. + +exercises_payload: + options: + shuffle_exercises: false + shuffle_groups: false + + exercise_groups: + - group: + - instructions: + - locale: fr_FR + value: Appuie sur une des flèches pour téléguider Leka dans la direction + de ton choix + - locale: en_US + value: Press one of the arrows to remotely guide Leka in the direction + of your choice + interface: remoteArrow diff --git a/Modules/ContentKit/Resources/Content/activities/remotes/remote_standard-E85F0498A29047E893631397592CC444.activity.yml b/Modules/ContentKit/Resources/Content/activities/remotes/remote_standard-E85F0498A29047E893631397592CC444.activity.yml new file mode 100644 index 0000000000..2aa30fe2fe --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/remotes/remote_standard-E85F0498A29047E893631397592CC444.activity.yml @@ -0,0 +1,93 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 + +uuid: E85F0498A29047E893631397592CC444 +name: remote_standard + +created_at: "2024-03-04T18:07:12.519606" +last_edited_at: "2024-03-05T09:42:38.078478" +status: published + +authors: + - leka + +skills: + - sensory_interaction + - gross_motor_skills + +tags: + - robot_movements + - robot_colors + +hmi: + - tablet_robot + +types: + - one_on_one + - group + +locales: + - en_US + - fr_FR + +l10n: + - locale: fr_FR + details: + icon: remote_standard + + title: Commande Standard + subtitle: null + + short_description: Contrôle Leka avec la télécommande, fais le se déplacer, + changer de couleur ou lance le renforçateur de ton choix + + description: | + La Commande Standard offre plusieurs fonctionnalités pour contrôler le robot : + - La Commande Fléchée pour téléguider le robot + - Les 5 renforçateurs de Leka + - Une commande colorée pour colorer le robot dans différentes couleurs + + instructions: | + - Pour faire se déplacer Leka, utiliser la Commande Fléchée en bas à gauche de l'écran. Appuyer sur les flèches pour choisir la direction du robot. + - Pour lancer un ou plusieurs renforçateurs, appuyer sur leurs différents icones à gauche de l'écran de l'iPad. + - Pour allumer le robot dans différentes couleurs, utiliser la commande colorée en bas à droite de l'écran. + + - locale: en_US + details: + icon: remote_standard + + title: Remote Standard + subtitle: null + + short_description: | + Control Leka with the remote control, make him move, change color or throw the reinforcer of your choice + + description: | + The Remote Standard offers several features to control the robot: + - The Remote Arrow to remotely guide the robot + - Leka’s 5 reinforcers + - The colorful command to color the robot in different colors + + instructions: | + - To move Leka, use the Remote Arrow at the bottom left of the screen. Press the arrows to choose the direction of the robot. + - To launch one or more reinforcers, press their different icons on the left of the iPad screen. + - To turn the robot on in different colors, use the colored command at the bottom right of the screen. + +exercises_payload: + options: + shuffle_exercises: false + shuffle_groups: false + + exercise_groups: + - group: + - instructions: + - locale: fr_FR + value: Contrôle Leka avec la télécommande, fais le se déplacer, changer + de couleur ou lance le renforçateur de ton choix + - locale: en_US + value: Control Leka with the remote control, make him move, change color + or throw the reinforcer of your choice + interface: remoteStandard diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_1-CBBCDFA8DC8C462794904F6E5E0638AB.activity.yml b/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_1-CBBCDFA8DC8C462794904F6E5E0638AB.activity.yml new file mode 100644 index 0000000000..2120f8caaf --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_1-CBBCDFA8DC8C462794904F6E5E0638AB.activity.yml @@ -0,0 +1,294 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 + +uuid: CBBCDFA8DC8C462794904F6E5E0638AB +name: color_bingo_1 + +created_at: "2024-03-04T18:07:12.519229" +last_edited_at: "2024-03-12T15:30:51.722290" +status: published + +authors: + - leka + +skills: + - recognition + - recognition/colors + - discrimination + +tags: + - colors + +hmi: + - tablet_robot + +types: + - one_on_one + +locales: + - en_US + - fr_FR + +l10n: + - locale: fr_FR + details: + icon: color_bingo_1 + + title: Loto des Couleurs 1 + subtitle: null + + short_description: | + Touche la couleur de Leka + + description: | + Leka s'allume dans une couleur. La personne accompagnée doit toucher la même couleur sur l'écran de l'iPad. + + instructions: | + - Leka s'allume dans une couleur + - Encourager la personne accompagnée à toucher la même couleur sur l'écran de l'iPad + - Lorsque la réponse est correct, Leka lance un renforçateur + + - locale: en_US + details: + icon: color_bingo_1 + + title: Color Bingo 1 + subtitle: null + + short_description: | + Touch the color of Leka + + description: | + Leka lights up in a color. The care receiver must touch the same color on the iPad screen. + + instructions: | + - Leka lights up in one color + - Encourage the care receiver to touch the same color on the iPad screen + - When the answer is correct, Leka launches a reinforcer + +exercises_payload: + options: + shuffle_exercises: true + shuffle_groups: false + + exercise_groups: + - group: + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: red + payload: + choices: + - value: red + type: color + is_right_answer: true + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: blue + payload: + choices: + - value: blue + type: color + is_right_answer: true + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: yellow + payload: + choices: + - value: yellow + type: color + is_right_answer: true + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: purple + payload: + choices: + - value: purple + type: color + is_right_answer: true + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: green + payload: + choices: + - value: green + type: color + is_right_answer: true + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: orange + payload: + choices: + - value: orange + type: color + is_right_answer: true + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: red + payload: + choices: + - value: red + type: color + is_right_answer: true + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: blue + payload: + choices: + - value: blue + type: color + is_right_answer: true + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: yellow + payload: + choices: + - value: yellow + type: color + is_right_answer: true + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: purple + payload: + choices: + - value: purple + type: color + is_right_answer: true + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: green + payload: + choices: + - value: green + type: color + is_right_answer: true + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: orange + payload: + choices: + - value: orange + type: color + is_right_answer: true diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_2-7960FD995EDC4C179A4BA4997E4C5A4F.activity.yml b/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_2-7960FD995EDC4C179A4BA4997E4C5A4F.activity.yml new file mode 100644 index 0000000000..c958e0dbe4 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_2-7960FD995EDC4C179A4BA4997E4C5A4F.activity.yml @@ -0,0 +1,309 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 + +uuid: 7960FD995EDC4C179A4BA4997E4C5A4F +name: color_bingo_2 + +created_at: "2024-03-04T18:07:12.519092" +last_edited_at: "2024-03-06T12:51:54.499454" +status: published + +authors: + - leka + +skills: + - recognition + - recognition/colors + - discrimination + +tags: + - colors + +hmi: + - tablet_robot + +types: + - one_on_one + +locales: + - en_US + - fr_FR + +l10n: + - locale: fr_FR + details: + icon: color_bingo_2 + + title: Loto des Couleurs 2 + subtitle: null + + short_description: | + Touche la couleur de Leka + + description: | + Leka s'allume dans une couleur. La personne accompagnée doit toucher la même couleur sur l'écran de l'iPad. + + instructions: | + - Leka s'allume dans une couleur + - Encourager la personne accompagnée à toucher la même couleur sur l'écran de l'iPad + - Lorsque la réponse est correct, Leka lance un renforçateur + + - locale: en_US + details: + icon: color_bingo_2 + + title: Color Bingo 2 + subtitle: null + + short_description: | + Touch the color of Leka + + description: | + Leka lights up in a color. The care receiver must touch the same color on the iPad screen. + + instructions: | + - Leka lights up in one color + - Encourage the care receiver to touch the same color on the iPad screen + - When the answer is correct, Leka launches a reinforcer + +exercises_payload: + options: + shuffle_exercises: true + shuffle_groups: false + + exercise_groups: + - group: + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: green + payload: + shuffle_choices: true + choices: + - value: green + type: color + is_right_answer: true + - value: purple + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: purple + payload: + shuffle_choices: true + choices: + - value: purple + type: color + is_right_answer: true + - value: yellow + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: orange + payload: + shuffle_choices: true + choices: + - value: orange + type: color + is_right_answer: true + - value: red + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: green + payload: + shuffle_choices: true + choices: + - value: green + type: color + is_right_answer: true + - value: red + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: touchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: green + payload: + shuffle_choices: true + choices: + - value: green + type: color + is_right_answer: true + - value: red + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: touchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: yellow + payload: + shuffle_choices: true + choices: + - value: yellow + type: color + is_right_answer: true + - value: red + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: touchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: blue + payload: + shuffle_choices: true + choices: + - value: blue + type: color + is_right_answer: true + - value: orange + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: touchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: yellow + payload: + shuffle_choices: true + choices: + - value: yellow + type: color + is_right_answer: true + - value: blue + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: touchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: orange + payload: + shuffle_choices: true + choices: + - value: orange + type: color + is_right_answer: true + - value: green + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: touchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: yellow + payload: + shuffle_choices: true + choices: + - value: yellow + type: color + is_right_answer: true + - value: orange + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: touchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: green + payload: + shuffle_choices: true + choices: + - value: green + type: color + is_right_answer: true + - value: yellow + type: color diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_3-E742B4AF67204DA68C08ADEC6126648E.activity.yml b/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_3-E742B4AF67204DA68C08ADEC6126648E.activity.yml new file mode 100644 index 0000000000..549d79b5ae --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_3-E742B4AF67204DA68C08ADEC6126648E.activity.yml @@ -0,0 +1,307 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 + +uuid: E742B4AF67204DA68C08ADEC6126648E +name: color_bingo_3 + +created_at: "2024-03-04T18:07:12.518998" +last_edited_at: "2024-03-06T12:51:54.499454" +status: published + +authors: + - leka + +skills: + - recognition + - recognition/colors + - discrimination + +tags: + - colors + +hmi: + - tablet_robot + +types: + - one_on_one + +locales: + - en_US + - fr_FR + +l10n: + - locale: fr_FR + details: + icon: color_bingo_3 + + title: Loto des Couleurs 3 + subtitle: null + + short_description: | + Touche la couleur de Leka + + description: | + Leka s'allume dans une couleur. La personne accompagnée doit toucher la même couleur sur l'écran de l'iPad. + + instructions: | + - Leka s'allume dans une couleur + - Encourager la personne accompagnée à toucher la même couleur sur l'écran de l'iPad + - Lorsque la réponse est correct, Leka lance un renforçateur + + - locale: en_US + details: + icon: color_bingo_3 + + title: Color Bingo 3 + subtitle: null + + short_description: | + Touch the color of Leka + + description: | + Leka lights up in a color. The care receiver must touch the same color on the iPad screen. + + instructions: | + - Leka lights up in one color + - Encourage the care receiver to touch the same color on the iPad screen + - When the answer is correct, Leka launches a reinforcer + +exercises_payload: + options: + shuffle_exercises: true + shuffle_groups: false + + exercise_groups: + - group: + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: red + payload: + shuffle_choices: true + choices: + - value: red + type: color + is_right_answer: true + - value: purple + type: color + - value: blue + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: purple + payload: + shuffle_choices: true + choices: + - value: purple + type: color + is_right_answer: true + - value: blue + type: color + - value: orange + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: red + payload: + shuffle_choices: true + choices: + - value: red + type: color + is_right_answer: true + - value: blue + type: color + - value: orange + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: green + payload: + shuffle_choices: true + choices: + - value: green + type: color + is_right_answer: true + - value: red + type: color + - value: purple + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: purple + payload: + shuffle_choices: true + choices: + - value: purple + type: color + is_right_answer: true + - value: yellow + type: color + - value: red + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: orange + payload: + shuffle_choices: true + choices: + - value: orange + type: color + is_right_answer: true + - value: purple + type: color + - value: green + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: purple + payload: + shuffle_choices: true + choices: + - value: purple + type: color + is_right_answer: true + - value: orange + type: color + - value: yellow + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: green + payload: + shuffle_choices: true + choices: + - value: green + type: color + is_right_answer: true + - value: purple + type: color + - value: yellow + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: red + payload: + shuffle_choices: true + choices: + - value: red + type: color + is_right_answer: true + - value: orange + type: color + - value: green + type: color + + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: red + payload: + shuffle_choices: true + choices: + - value: red + type: color + is_right_answer: true + - value: orange + type: color + - value: yellow + type: color diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_4-A7584D692B23422BB12683FA7BE393BE.activity.yml b/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_4-A7584D692B23422BB12683FA7BE393BE.activity.yml new file mode 100644 index 0000000000..be02c151fb --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_4-A7584D692B23422BB12683FA7BE393BE.activity.yml @@ -0,0 +1,318 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 + +uuid: A7584D692B23422BB12683FA7BE393BE +name: color_bingo_4 + +created_at: "2024-03-08T11:09:22.386489" +last_edited_at: "2024-03-08T11:12:02.081585" +status: published + +authors: + - leka + +skills: + - recognition + - recognition/colors + - discrimination + +tags: + - colors + +hmi: + - tablet_robot + +types: + - one_on_one + +locales: + - en_US + - fr_FR + +l10n: + - locale: fr_FR + details: + icon: color_bingo_4 + + title: Loto des Couleurs 4 + subtitle: null + + short_description: | + Touche la couleur de Leka + + description: | + Leka s'allume dans une couleur. La personne accompagnée doit toucher la même couleur sur l'écran de l'iPad. + + instructions: | + - Leka s'allume dans une couleur + - Encourager la personne accompagnée à toucher la même couleur sur l'écran de l'iPad + - Lorsque la réponse est correct, Leka lance un renforçateur + + - locale: en_US + details: + icon: color_bingo_4 + + title: Color Bingo 4 + subtitle: null + + short_description: | + Touch the color of Leka + + description: | + Leka lights up in a color. The care receiver must touch the same color on the iPad screen. + + instructions: | + - Leka lights up in one color + - Encourage the care receiver to touch the same color on the iPad screen + - When the answer is correct, Leka launches a reinforcer + +exercises_payload: + options: + shuffle_exercises: true + shuffle_groups: false + + exercise_groups: + - group: + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: orange + payload: + shuffle_choices: true + choices: + - value: orange + type: color + is_right_answer: true + - value: red + type: color + - value: blue + type: color + - value: purple + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: green + payload: + shuffle_choices: true + choices: + - value: green + type: color + is_right_answer: true + - value: red + type: color + - value: yellow + type: color + - value: purple + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: purple + payload: + shuffle_choices: true + choices: + - value: purple + type: color + is_right_answer: true + - value: blue + type: color + - value: orange + type: color + - value: green + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: yellow + payload: + shuffle_choices: true + choices: + - value: yellow + type: color + is_right_answer: true + - value: blue + type: color + - value: orange + type: color + - value: purple + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: green + payload: + shuffle_choices: true + choices: + - value: green + type: color + is_right_answer: true + - value: blue + type: color + - value: orange + type: color + - value: red + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: red + payload: + shuffle_choices: true + choices: + - value: red + type: color + is_right_answer: true + - value: blue + type: color + - value: yellow + type: color + - value: orange + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: blue + payload: + shuffle_choices: true + choices: + - value: blue + type: color + is_right_answer: true + - value: red + type: color + - value: green + type: color + - value: yellow + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: red + payload: + shuffle_choices: true + choices: + - value: red + type: color + is_right_answer: true + - value: orange + type: color + - value: green + type: color + - value: yellow + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: blue + payload: + shuffle_choices: true + choices: + - value: blue + type: color + is_right_answer: true + - value: yellow + type: color + - value: orange + type: color + - value: green + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: purple + payload: + shuffle_choices: true + choices: + - value: purple + type: color + is_right_answer: true + - value: red + type: color + - value: blue + type: color + - value: yellow + type: color diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_5-651AD9E7F59249EFA3A48F23A78134A2.activity.yml b/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_5-651AD9E7F59249EFA3A48F23A78134A2.activity.yml new file mode 100644 index 0000000000..dea42ec07b --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_5-651AD9E7F59249EFA3A48F23A78134A2.activity.yml @@ -0,0 +1,234 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 + +uuid: 651AD9E7F59249EFA3A48F23A78134A2 +name: color_bingo_5 + +created_at: "2024-03-08T11:09:22.386406" +last_edited_at: "2024-03-08T11:12:02.081585" +status: published + +authors: + - leka + +skills: + - recognition + - recognition/colors + - discrimination + +tags: + - colors + +hmi: + - tablet_robot + +types: + - one_on_one + +locales: + - en_US + - fr_FR + +l10n: + - locale: fr_FR + details: + icon: color_bingo_5 + + title: Loto des Couleurs 5 + subtitle: null + + short_description: | + Touche la couleur de Leka + + description: | + Leka s'allume dans une couleur. La personne accompagnée doit toucher la même couleur sur l'écran de l'iPad. + + instructions: | + - Leka s'allume dans une couleur + - Encourager la personne accompagnée à toucher la même couleur sur l'écran de l'iPad + - Lorsque la réponse est correct, Leka lance un renforçateur + + - locale: en_US + details: + icon: color_bingo_5 + + title: Color Bingo 5 + subtitle: null + + short_description: | + Touch the color of Leka + + description: | + Leka lights up in a color. The care receiver must touch the same color on the iPad screen. + + instructions: | + - Leka lights up in one color + - Encourage the care receiver to touch the same color on the iPad screen + - When the answer is correct, Leka launches a reinforcer + +exercises_payload: + options: + shuffle_exercises: true + shuffle_groups: false + + exercise_groups: + - group: + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: purple + payload: + shuffle_choices: true + choices: + - value: purple + type: color + is_right_answer: true + - value: red + type: color + - value: blue + type: color + - value: orange + type: color + - value: green + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: red + payload: + shuffle_choices: true + choices: + - value: red + type: color + is_right_answer: true + - value: purple + type: color + - value: blue + type: color + - value: orange + type: color + - value: yellow + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: blue + payload: + shuffle_choices: true + choices: + - value: blue + type: color + is_right_answer: true + - value: red + type: color + - value: purple + type: color + - value: orange + type: color + - value: green + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: orange + payload: + shuffle_choices: true + choices: + - value: orange + type: color + is_right_answer: true + - value: purple + type: color + - value: red + type: color + - value: blue + type: color + - value: yellow + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: green + payload: + shuffle_choices: true + choices: + - value: green + type: color + is_right_answer: true + - value: red + type: color + - value: blue + type: color + - value: purple + type: color + - value: orange + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: yellow + payload: + shuffle_choices: true + choices: + - value: yellow + type: color + is_right_answer: true + - value: blue + type: color + - value: red + type: color + - value: orange + type: color + - value: green + type: color diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_6-EFC7F64F51FD4D68863749C1C278BD1F.activity.yml b/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_6-EFC7F64F51FD4D68863749C1C278BD1F.activity.yml new file mode 100644 index 0000000000..1aabf47515 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/color_bingo_6-EFC7F64F51FD4D68863749C1C278BD1F.activity.yml @@ -0,0 +1,218 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 + +uuid: EFC7F64F51FD4D68863749C1C278BD1F +name: color_bingo_6 + +created_at: "2024-03-08T11:09:22.386263" +last_edited_at: "2024-03-12T15:23:28.999813" +status: published + +authors: + - leka + +skills: + - recognition + - recognition/colors + - discrimination + +tags: + - colors + +hmi: + - tablet_robot + +types: + - one_on_one + +locales: + - en_US + - fr_FR + +l10n: + - locale: fr_FR + details: + icon: color_bingo_6 + + title: Loto des Couleurs 6 + subtitle: null + + short_description: | + Touche la couleur de Leka + + description: | + Leka s'allume dans une couleur. La personne accompagnée doit toucher la même couleur sur l'écran de l'iPad. + + instructions: | + - Leka s'allume dans une couleur + - Encourager la personne accompagnée à toucher la même couleur sur l'écran de l'iPad + - Lorsque la réponse est correct, Leka lance un renforçateur + + - locale: en_US + details: + icon: color_bingo_6 + + title: Color Bingo 6 + subtitle: null + + short_description: | + Touch the color of Leka + + description: | + Leka lights up in a color. The care receiver must touch the same color on the iPad screen. + + instructions: | + - Leka lights up in one color + - Encourage the care receiver to touch the same color on the iPad screen + - When the answer is correct, Leka launches a reinforcer + +exercises_payload: + options: + shuffle_exercises: true + shuffle_groups: false + + exercise_groups: + - group: + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: purple + payload: + shuffle_choices: true + choices: + - value: purple + type: color + is_right_answer: true + - value: red + type: color + - value: blue + type: color + - value: orange + type: color + - value: green + type: color + - value: yellow + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: red + payload: + shuffle_choices: true + choices: + - value: red + type: color + is_right_answer: true + - value: purple + type: color + - value: blue + type: color + - value: orange + type: color + - value: green + type: color + - value: yellow + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: blue + payload: + shuffle_choices: true + choices: + - value: blue + type: color + is_right_answer: true + - value: purple + type: color + - value: red + type: color + - value: orange + type: color + - value: green + type: color + - value: yellow + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: orange + payload: + shuffle_choices: true + choices: + - value: orange + type: color + is_right_answer: true + - value: purple + type: color + - value: red + type: color + - value: blue + type: color + - value: orange + type: color + - value: yellow + type: color + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: green + payload: + shuffle_choices: true + choices: + - value: green + type: color + is_right_answer: true + - value: purple + type: color + - value: red + type: color + - value: blue + type: color + - value: orange + type: color + - value: yellow + type: color diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/dance_freeze-6E2F7D56726C419EA534C614F777D934.activity.yml b/Modules/ContentKit/Resources/Content/activities/standalones/dance_freeze-6E2F7D56726C419EA534C614F777D934.activity.yml new file mode 100644 index 0000000000..1078086b79 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/dance_freeze-6E2F7D56726C419EA534C614F777D934.activity.yml @@ -0,0 +1,94 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 + +uuid: 6E2F7D56726C419EA534C614F777D934 +name: dance_freeze + +created_at: "2024-03-01T14:00:38.160403" +last_edited_at: "2024-03-06T12:51:54.499453" +status: published + +authors: + - leka + +skills: + - gross_motor_skills + - inhibition + +tags: + - music + - movements + - dancing + +hmi: + - robot + +types: + - group + +locales: + - en_US + - fr_FR + +l10n: + - locale: fr_FR + details: + icon: dance_freeze + + title: Dance Freeze + subtitle: Bouge avec Leka ! + + short_description: | + Bouger avec Leka au rythme de la musique et faire la statue quand il s'arrête. + + description: | + *Dance Freeze* est une activité ludique où Leka, le robot, se met en mouvement au rythme de la musique. Quand la musique s'arrête, Leka s'immobilise et tous les participants doivent faire la statue. Le jeu propose deux modes : un mode manuel, où un professionnel contrôle quand Leka s'arrête, et un mode automatique où Leka s'arrête de lui-même. + + instructions: | + - Sélectionner le mode "rotation" pour que le robot tourne sur lui-même ou le mode "déplacement" pour qu'il se déplace dans la pièce. + - Sélectionner la musique souhaitée dans la liste. + - Appuyer sur Jouer - mode manuel pour contrôler vous-même quand Leka s'arrête et repart. + - Appuyer sur Jouer - mode auto pour lancer le mode automatique où Leka s'arrête et repart de lui-même de manière aléatoire. + - Encourager les personnes accompagnées à se mouvoir en même temps que le robot et à s'arrêter lorsqu'il s'immobilise. + + - locale: en_US + details: + icon: dance_freeze + + title: Dance Freeze + subtitle: Move with Leka! + + short_description: | + Move with Leka to the rhythm of the music and act like a statue when he stops. + + description: | + *Dance Freeze* is a playful activity where Leka, the robot, moves to the rhythm of the music. When the music stops, Leka freezes, and all participants must strike a pose. The game offers two modes: a manual mode, where a professional controls when Leka stops, and an automatic mode where Leka stops by itself. + + instructions: | + - Select the "rotation" mode for the robot to spin in place or the "movement" mode for it to move around the room. + - Choose the desired music from the list. + - Press Play - manual mode to control when Leka stops and starts yourself. + - Press Play - auto mode to activate the automatic mode where Leka stops and starts on its own randomly. + - Encourage the care receivers to move along with the robot and to stop when it freezes. + +exercises_payload: + options: + shuffle_exercises: false + shuffle_groups: false + + exercise_groups: + - group: + - instructions: null + interface: danceFreeze + payload: + songs: + - earlyBird + - emptyPage + - gigglySquirrel + - handsOn + - happyDays + - inTheGame + - littleByLittle diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/discover_leka-89CEA9FE20624659B1307B633641BC90.activity.yml b/Modules/ContentKit/Resources/Content/activities/standalones/discover_leka-89CEA9FE20624659B1307B633641BC90.activity.yml new file mode 100644 index 0000000000..d6ec96f04a --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/discover_leka-89CEA9FE20624659B1307B633641BC90.activity.yml @@ -0,0 +1,86 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 + +uuid: 89CEA9FE20624659B1307B633641BC90 +name: discover_leka + +created_at: "2024-03-01T14:00:38.160396" +last_edited_at: "2024-03-06T12:51:54.499454" +status: published + +authors: + - leka + +skills: + - familiarization_with_leka + +tags: + - pairing + - familiarization + - discover_leka + +hmi: + - robot + +types: + - one_on_one + +locales: + - en_US + - fr_FR + +l10n: + - locale: fr_FR + details: + icon: discover_leka + + title: Découvre Leka + subtitle: Familiarise toi avec Leka ! + + short_description: | + Découvrir Leka et ses stimulations sensorielles : lumières, vibrations, mouvements + + description: | + *Découvrir Leka* est une activité interactive conçue pour que les personnes puissent se familiariser avec le robot et être à l'aise en sa présence. Au cours de l'activité, le robot exécute divers mouvements, émet des lumières, des vibrations et affiche des couleurs. + + instructions: | + - Cliquez sur le bouton "Jouer" situé à gauche de l'écran de l'iPad. + - Encouragez la personne à focaliser son attention sur le robot. + - Appuyez sur le bouton "Pause" si nécessaire. + - Cliquez sur le bouton "Stop" lorsque vous souhaitez mettre fin à l'activité. + + - locale: en_US + details: + icon: discover_leka + + title: Discover Leka + subtitle: Get familiar with Leka! + + short_description: | + Discover Leka and its sensory stimulations: lights, vibrations, movements + + description: | + *Discover Leka* is an interactive activity designed to help care receivers become familiar with the robot and feel comfortable in its presence. Throughout the activity, the robot performs various movements, emits lights, vibrations, and displays colors. + + instructions: | + - Press the "Play" button located on the left side of the iPad screen. + - Prompt the person to focus their attention on the robot. + - Tap the "Pause" button if necessary. + - Press the "Stop" button when you wish to end the activity. + +exercises_payload: + options: + shuffle_exercises: false + shuffle_groups: false + + exercise_groups: + - group: + - instructions: + - locale: fr_FR + value: Observe Leka + - locale: en_US + value: Observe Leka + interface: pairing diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_1.activity.icon.png b/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_1.activity.icon.png new file mode 100755 index 0000000000..2bddde2321 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_1.activity.icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec92d18a363fa364412d533f5816e44de37b6dd3fa0e559208f7145fd33dffe0 +size 13708 diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_2.activity.icon.png b/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_2.activity.icon.png new file mode 100755 index 0000000000..9636b24629 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_2.activity.icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36848ffade665d6f7023d8dc9b32890af8fc5529e0a90eed52d8d1edfb0d4c2b +size 14162 diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_3.activity.icon.png b/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_3.activity.icon.png new file mode 100755 index 0000000000..84ff3c0fa2 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_3.activity.icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:391441f1893ebc85e936ffda40723964029215e7f7204a9ee7b05ad3385bd03d +size 14765 diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_4.activity.icon.png b/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_4.activity.icon.png new file mode 100755 index 0000000000..ed0f1038dd --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_4.activity.icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a3d4ba1ebdb4c35168511ebf1954b786b3f2eaae9a388d779bb5e30968358d1 +size 15237 diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_5.activity.icon.png b/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_5.activity.icon.png new file mode 100755 index 0000000000..20e9979d7b --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_5.activity.icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5d01c4759f0a3e22e6547a3ca0a0594c766e4713d98d7e36a59b4a0c0e47834 +size 15681 diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_6.activity.icon.png b/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_6.activity.icon.png new file mode 100755 index 0000000000..1eaefab998 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/icons/color_bingo_6.activity.icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e90386ccda6ea1444376062d920897a2e3065a4c3a01fb092b39d419e353c85a +size 16175 diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/icons/dance_freeze.activity.icon.png b/Modules/ContentKit/Resources/Content/activities/standalones/icons/dance_freeze.activity.icon.png new file mode 100755 index 0000000000..4d8a9b11f8 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/icons/dance_freeze.activity.icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e51f966e7f1fe973b492468e88aaf2f12b9f6abd834138cc58feee8fac75a7d +size 11748 diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/icons/discover_leka.activity.icon.png b/Modules/ContentKit/Resources/Content/activities/standalones/icons/discover_leka.activity.icon.png new file mode 100755 index 0000000000..0ffccd8347 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/icons/discover_leka.activity.icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cb4f7aee7776ca9894b7907a227726b358e97f96fbbcff633954852a9835903 +size 18893 diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/icons/melody.activity.icon.png b/Modules/ContentKit/Resources/Content/activities/standalones/icons/melody.activity.icon.png new file mode 100755 index 0000000000..0f285e4888 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/icons/melody.activity.icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3503f9965c185fb15de3ab6d9054cb2b03f8c5e5856914798c8ae8d1013273e2 +size 11830 diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/icons/placeholder.activity.icon.png b/Modules/ContentKit/Resources/Content/activities/standalones/icons/placeholder.activity.icon.png new file mode 100644 index 0000000000..127b735485 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/icons/placeholder.activity.icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0dc3b16020e264f7a7bbfa8e5ffaa30ef4a362954161694c9b3c34cda8c16c4d +size 29941 diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/icons/xylophone_heptatonic.activity.icon.png b/Modules/ContentKit/Resources/Content/activities/standalones/icons/xylophone_heptatonic.activity.icon.png new file mode 100755 index 0000000000..a093f2fe8d --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/icons/xylophone_heptatonic.activity.icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d54689d4e66bf04278c038d244d8352ef0994088510da68f3677604a72b0243 +size 13761 diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/icons/xylophone_pentatonic.activity.icon.png b/Modules/ContentKit/Resources/Content/activities/standalones/icons/xylophone_pentatonic.activity.icon.png new file mode 100755 index 0000000000..3fc85463a8 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/icons/xylophone_pentatonic.activity.icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8e370876fb77f1c3abd054099e1c36393b4a38a2a3d6861c02ec77deda8c82f +size 8609 diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/melody-CB995F829411449AB32E6B8A688E3DDB.activity.yml b/Modules/ContentKit/Resources/Content/activities/standalones/melody-CB995F829411449AB32E6B8A688E3DDB.activity.yml new file mode 100644 index 0000000000..8c4fdfb9aa --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/melody-CB995F829411449AB32E6B8A688E3DDB.activity.yml @@ -0,0 +1,96 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 + +uuid: CB995F829411449AB32E6B8A688E3DDB +name: melody + +created_at: "2024-03-04T18:07:12.519606" +last_edited_at: "2024-03-06T12:51:54.499453" +status: published + +authors: + - leka + +skills: + - recognition + - recognition/colors + - discrimination + - attention + - attention/sustained_attention + - sensory_interaction + +tags: + - colors + - music + - sounds + +hmi: + - tablet_robot + +types: + - one_on_one + +locales: + - en_US + - fr_FR + +l10n: + - locale: fr_FR + details: + icon: melody + + title: Mélodie + subtitle: null + + short_description: | + Joue les notes de la même couleur que Leka + + description: | + Cette activité invite la personne accompagnée à s'engager dans une expérience musicale interactive. Après avoir sélectionné une musique, lancé la séquence et écouté la mélodie, la personne accompagnée doit associer les touches colorées sur l'iPad aux couleurs du robot. + + instructions: | + - Choix du clavier : Sélectionner le clavier partiel pour limiter les notes à celles de la mélodie, simplifiant ainsi la discrimination pour la personne accompagnée. Choisir le clavier entier pour accéder à l'ensemble de la gamme heptatonique. + - Choisir la musique que vous souhaitez voir interprétée et reproduite. + - Appuyer sur "Jouer" pour lancer la séquence. + - Encourager la personne accompagnée à écouter attentivement la mélodie provenant de l'iPad. + - Demander à la personne accompagnée d'appuyer sur la touche de la même couleur que le robot. + - À la fin, la séquence de notes colorées est rejouée, accompagnée de la mélodie. + + - locale: en_US + details: + icon: melody + + title: Melody + subtitle: null + + short_description: | + Play notes of the same color as Leka + + description: | + This activity invites the care receiver to engage in an interactive musical experience. After selecting music, starting the sequence and listening to the melody, the care receiver must associate the colored keys on the iPad with the colors of the robot. + + + instructions: | + - Choice of keyboard: Select the partial keyboard to limit the notes to those of the melody, thus simplifying discrimination for the care receiver. Choose the entire keyboard to access the entire heptatonic scale. + - Choose the music you want to see performed and reproduced. + - Press “Play” to start the sequence. + - Encourage the care receiver to listen carefully to the melody coming from the iPad. + - Ask the care receiver to press the button of the same color as the robot. + - At the end, the sequence of colored notes is played again, accompanied by the melody. + +exercises_payload: + options: + shuffle_exercises: false + shuffle_groups: false + + exercise_groups: + - group: + - instructions: + - locale: fr_FR + value: Joue les notes de la même couleur que Leka + - locale: en_US + value: Play the notes of the same color as Leka + interface: melody diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/xylophone_heptatonic-65193102AE2947439ABB82D5891EDDBA.activity.yml b/Modules/ContentKit/Resources/Content/activities/standalones/xylophone_heptatonic-65193102AE2947439ABB82D5891EDDBA.activity.yml new file mode 100644 index 0000000000..c6b7e6900e --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/xylophone_heptatonic-65193102AE2947439ABB82D5891EDDBA.activity.yml @@ -0,0 +1,96 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 +uuid: 65193102AE2947439ABB82D5891EDDBA +name: xylophone_heptatonic + +created_at: "2024-03-01T14:00:38.160397" +last_edited_at: "2024-03-06T12:51:54.499453" +status: published + +authors: + - leka + +skills: + - relationship_tablet_robot + +tags: + - colors + - music + +hmi: + - tablet_robot + +types: + - one_on_one + +locales: + - en_US + - fr_FR + +l10n: + - locale: fr_FR + details: + icon: xylophone_heptatonic + + title: Xylophone heptatonique + subtitle: null + + short_description: | + Jouer du Xylophone avec une gamme étendue (gamme heptatonique). + + description: | + L'activité xylophone permet à la personne de jouer de la musique en touchant les touches du xylophone affichées sur l'iPad. + Chaque pression sur une touche colore Leka dans la même couleur. Cette activité favorise la compréhension du lien de cause à + effet entre la tablette et le robot tout en offrant une expérience sensorielle enrichissante pour la personne. + + Avec le xylophone heptatonique, explorez une gamme plus étendue de sons, du do au do. + + Les possibilités musicales sont riches, mais attention aux fausses notes possibles. Similaire aux touches blanches du piano, cet instrument invite à l'exploration musicale avec un peu plus de complexité. + + instructions: | + - Encourager la personne à toucher les touches colorées affichées sur l'iPad pour produire des notes de musique. + - Encourager la personne à observer comment Leka change de couleur pour correspondre à la touche pressée. + + - locale: en_US + details: + icon: xylophone_heptatonic + + title: Heptatonic xylophone + subtitle: null + + short_description: | + Play the Xylophone with an extended range (heptatonic range). + + description: | + The xylophone activity allows the person to play music by touching the xylophone keys displayed on the iPad. + Each press on a key colors Leka in the same color. This activity promotes understanding of the cause-and-effect relationship + between the tablet and the robot while offering a sensory-enriching experience for the care receiver. + + With the heptatonic xylophone, explore a wider range of sounds, from C to C. + + The musical possibilities are rich, but be careful of possible false notes. Similar to the white keys of the piano, + this instrument invites musical exploration with a little more complexity. + + instructions: | + - Encourage the care receiver to touch the colored keys displayed on the iPad to produce musical notes. + - Encourage the care receiver to observe how Leka changes color to match the pressed key. + +exercises_payload: + options: + shuffle_exercises: false + shuffle_groups: false + + exercise_groups: + - group: + - instructions: + - locale: fr_FR + value: Joue du Xylophone avec Leka + - locale: en_US + value: Play the Xylophone with Leka + interface: musicalInstruments + payload: + instrument: xylophone + scale: majorHeptatonic diff --git a/Modules/ContentKit/Resources/Content/activities/standalones/xylophone_pentatonic-7C1DDED6C6D44913B9A7D4EB4AEE8F6B.activity.yml b/Modules/ContentKit/Resources/Content/activities/standalones/xylophone_pentatonic-7C1DDED6C6D44913B9A7D4EB4AEE8F6B.activity.yml new file mode 100644 index 0000000000..fc7ca48d9f --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/standalones/xylophone_pentatonic-7C1DDED6C6D44913B9A7D4EB4AEE8F6B.activity.yml @@ -0,0 +1,86 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 +uuid: 7C1DDED6C6D44913B9A7D4EB4AEE8F6B +name: xylophone_pentatonic + +created_at: "2024-03-01T14:00:38.160396" +last_edited_at: "2024-03-06T12:51:54.499454" +status: published + +authors: + - leka + +skills: + - relationship_tablet_robot + +tags: + - colors + - music + +hmi: + - tablet_robot + +types: + - one_on_one + +locales: + - en_US + - fr_FR + +l10n: + - locale: fr_FR + details: + icon: xylophone_pentatonic + + title: Xylophone pentatonique + subtitle: null + + short_description: | + Jouer du xylophone sans fausse notes (gamme pentatonique) + + description: | + L'activité xylophone permet à la personne de jouer de la musique en touchant les touches du xylophone affichées sur l'iPad. Chaque pression sur une touche colore Leka dans la même couleur. Cette activité favorise la compréhension du lien de cause à effet entre la tablette et le robot tout en offrant une expérience sensorielle enrichissante pour la personne. + Facile à jouer, le xylophone pentatonique offre des sons harmonieux sans risque de fausses notes. Ses touches rappellent les touches noires du piano, offrant une expérience musicale simple et plaisante. + + instructions: | + - Encourager la personne à toucher les touches colorées affichées sur l'iPad pour produire des notes de musique. + - Encourager la personne à observer comment Leka change de couleur pour correspondre à la touche pressée. + + - locale: en_US + details: + icon: xylophone_pentatonic + + title: Pentatonic xylophone + subtitle: null + + short_description: | + Playing the Xylophone without false notes (pentatonic scale) + + description: | + The xylophone activity allows the person to play music by touching the xylophone keys displayed on the iPad. Each press on a key colors Leka in the same color. + This activity promotes understanding of the cause-and-effect relationship between the tablet and the robot while offering a sensory-enriching experience for the care receiver. Easy to play, + the pentatonic xylophone offers harmonious sounds without the risk of false notes. Its keys are reminiscent of the black keys of the piano, offering a simple and pleasant musical experience. + + instructions: | + - Encourage the care receiver to touch the colored keys displayed on the iPad to play musical notes. + - Encourage the care receiver to observe how Leka changes color to match the pressed key. + +exercises_payload: + options: + shuffle_exercises: false + shuffle_groups: false + + exercise_groups: + - group: + - instructions: + - locale: fr_FR + value: Joue du Xylophone avec Leka + - locale: en_US + value: Play the Xylophone with Leka + interface: musicalInstruments + payload: + instrument: xylophone + scale: majorPentatonic diff --git a/Modules/ContentKit/Resources/Content/activities/templates/activity_template-0123456789ABCDEF0123456789ABCDEF.activity.yml b/Modules/ContentKit/Resources/Content/activities/templates/activity_template-0123456789ABCDEF0123456789ABCDEF.activity.yml new file mode 100644 index 0000000000..ef64a8c923 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/templates/activity_template-0123456789ABCDEF0123456789ABCDEF.activity.yml @@ -0,0 +1,159 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 + +uuid: 0123456789ABCDEF0123456789ABCDEF # {{💡TO_DEFINE}} +name: activity_template # {{💡TO_DEFINE}} + +created_at: "2024-02-28T14:49:32.973012" +last_edited_at: "2024-03-12T15:04:49.139093" +status: published + +authors: # {{💡TO_DEFINE}} + - leka + +skills: # {{💡TO_DEFINE}} + - recognition + +tags: + - tag_one + - tag_two + - tag_three + +hmi: # {{💡TO_DEFINE}} + - tablet + +types: # {{💡TO_DEFINE}} + - one_on_one + - group + +locales: + - en_US + - fr_FR + +l10n: + - locale: fr_FR + details: + icon: activity_template_icon_name # {{💡TO_DEFINE}} + + title: Titre de l'activité # {{💡TO_DEFINE}} + subtitle: Sous-titre de l'activité # {{💡TO_DEFINE}} + + short_description: | # {{💡TO_DEFINE}} + Courte description de l'activité + + description: | # {{💡TO_DEFINE}} + Longue description de l'activité + + instructions: | # {{💡TO_DEFINE}} + ## Instructions de l'activité + + bla bla bla + + - locale: en_US + details: + icon: activity_template_icon_name # {{💡TO_DEFINE}} + + title: Activity title # {{💡TO_DEFINE}} + subtitle: Activity subtitle # {{💡TO_DEFINE}} + + short_description: | # {{💡TO_DEFINE}} + Short description of the activity + + description: | # {{💡TO_DEFINE}} + Long description of the activity + + instructions: | # {{💡TO_DEFINE}} + ## Activity instructions + + bla bla bla + +exercises_payload: + options: + shuffle_exercises: false + shuffle_groups: false + + exercise_groups: + - group: + - instructions: + - locale: fr_FR + value: Touche la couleur de Leka + - locale: en_US + value: Touch the color of Leka + interface: robotThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: robot + value: + type: color + value: green + payload: + shuffle_choices: true + choices: + - value: green + type: color + is_right_answer: true + - value: purple + type: color + + - instructions: + - locale: fr_FR + value: Touch le rond jaune + - locale: en_US + value: Touch the yellow circle + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: yellow + type: color + is_right_answer: true + - value: red + type: color + - value: green + type: color + - value: blue + type: color + + - instructions: + - locale: fr_FR + value: Touch la pastèque + - locale: en_US + value: Touch the watermelon + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: 🍉 + type: emoji + is_right_answer: true + - value: 🍌 + type: emoji + - value: 🍒 + type: emoji + - value: 🥝 + type: emoji + + - instructions: + - locale: fr_FR + value: Touch le carré + - locale: en_US + value: Touch the square + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: circle + type: sfsymbol + - value: square + type: sfsymbol + is_right_answer: true + - value: triangle + type: sfsymbol + - value: rhombus + type: sfsymbol diff --git a/Modules/ContentKit/Resources/Content/activities/templates/exercise_templates.yml b/Modules/ContentKit/Resources/Content/activities/templates/exercise_templates.yml new file mode 100644 index 0000000000..1c5a270825 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/templates/exercise_templates.yml @@ -0,0 +1,72 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +--- +# MARK: - touchToSelect - findTheRightAnswers + +- instructions: + - locale: fr_FR + value: Touch le rond jaune + - locale: en_US + value: Touch the yellow circle + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true # true or false + choices: + - value: yellow + type: color # available types: color, emoji, sfsymbol, image + - value: red + type: color + - value: green + type: color + is_right_answer: true # more than one is possible, even all of the choices can be right + - value: blue + type: color + +--- +# MARK: - observeThenTouchToSelect - findTheRightAnswers + +- instructions: + - locale: fr_FR + value: Regarde et touche la même couleur + - locale: en_US + value: Observe and touch the same color + interface: observeThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: ipad + value: + type: image + value: landscape_blue # name of the file + payload: + choices: + - value: blue + type: color + is_right_answer: true + - value: red + type: color + +--- +# MARK: - listenThenTouchToSelect - findTheRightAnswers + +- instructions: + - locale: fr_FR + value: Regarde et touche la même couleur + - locale: en_US + value: Observe and touch the same color + interface: listenThenTouchToSelect + gameplay: findTheRightAnswers + action: + type: ipad + value: + type: audio + value: sound_guitar # name of the file + payload: + choices: + - value: instruments_guitar_no_background + type: image + is_right_answer: true + - value: instruments_violin_no_background + type: image diff --git a/Modules/ContentKit/Resources/Content/activities/templates/icons/activity_template_icon_name.activity.icon.png b/Modules/ContentKit/Resources/Content/activities/templates/icons/activity_template_icon_name.activity.icon.png new file mode 100644 index 0000000000..09095598d8 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/templates/icons/activity_template_icon_name.activity.icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05df4f13a57b6ae51a72287caeb7ecae72d29facb9a13ab7be7737d6d4a967d3 +size 685037 diff --git a/Modules/ContentKit/Resources/Content/activities/templates/icons/sample_1.activity.icon.png b/Modules/ContentKit/Resources/Content/activities/templates/icons/sample_1.activity.icon.png new file mode 100644 index 0000000000..09095598d8 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/templates/icons/sample_1.activity.icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05df4f13a57b6ae51a72287caeb7ecae72d29facb9a13ab7be7737d6d4a967d3 +size 685037 diff --git a/Modules/ContentKit/Resources/Content/activities/templates/icons/sample_2.activity.icon.png b/Modules/ContentKit/Resources/Content/activities/templates/icons/sample_2.activity.icon.png new file mode 100644 index 0000000000..b9c92e79d0 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/templates/icons/sample_2.activity.icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5fce2dcc029ed929903ffe05fe58f495324ac2e37bf6df8a81462cb57bd81d6 +size 391526 diff --git a/Modules/ContentKit/Resources/Content/activities/templates/sample_1-5A670B59EB214A6AA11AB39D64D63990.activity.yml b/Modules/ContentKit/Resources/Content/activities/templates/sample_1-5A670B59EB214A6AA11AB39D64D63990.activity.yml new file mode 100644 index 0000000000..a554b39ee9 --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/templates/sample_1-5A670B59EB214A6AA11AB39D64D63990.activity.yml @@ -0,0 +1,295 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 + +uuid: 5A670B59EB214A6AA11AB39D64D63990 +name: sample_1 + +created_at: "2024-02-28T14:49:32.982712" +last_edited_at: "2024-03-05T09:59:06.753419" +status: published + +authors: + - leka + - aurore_kiesler + - julie_tuil + +skills: + - spatial_understanding + - recognition/animals + - communication/non_verbal_communication/gestures + +tags: + - tag_one + - tag_two + - tag_three + +hmi: + - robot + - magic_cards + - tablet_robot + +types: + - one_on_one + - group + +locales: + - en_US + - fr_FR + +l10n: + - locale: fr_FR + details: + icon: sample_1 + + title: Activité d'exemple 1 + subtitle: pour le développement + + short_description: > + Nunc Corinthiaci anilem, rerum primo et ambos fata? Et Drya aliis abest, *adeunt + deprenduntur* vidi age: venit *cuncti*, aures. Sentit tale nubibus! + + description: | + Lorem **markdownum arduus salve** sermone quanto fuit: ait nunc placitas precor + **Mercurium novi**: terra nec. Nullo si auctor tenentis **verbis**; sed ore. + Nisi sermone procul, et diu turbantes, opem nitidis, aera ad esse Pico refugit + iugulum? Gradus non verbis humana **rursus aeternum vincetis** gaudet inpia + puro, est. + + instructions: | + ## Longues instructions en markdown plus complexe si on veut + + Lorem markdownum recepta avidum, missa de quam patientia, antris: cum defuit, + Titan repetemus nomine, ignare. Quod ad aura, et non quod vidisse utque ulla: + + - Pro inposuit tibi orsa tum artes ferox + - Acmon plausu qua agrestum situs virgo in + - Vacuus a pendens rostro non si pharetrae + - Haeremusque quos auxiliaris coniunx + - Repulsa impediunt munera teneri fallebat + - Bracchia frustra telo Iovis faucibus casus + + - locale: en_US + details: + icon: sample_1 + + title: Sample activity 1 + subtitle: for development + + short_description: > + Nunc Corinthiaci anilem, rerum primo et ambos fata? Et Drya aliis abest, *adeunt + deprenduntur* vidi age: venit *cuncti*, aures. Sentit tale nubibus! + + description: | + Lorem **markdownum arduus salve** sermone quanto fuit: ait nunc placitas precor + **Mercurium novi**: terra nec. Nullo si auctor tenentis **verbis**; sed ore. + Nisi sermone procul, et diu turbantes, opem nitidis, aera ad esse Pico refugit + iugulum? Gradus non verbis humana **rursus aeternum vincetis** gaudet inpia + puro, est. + + instructions: | + ## Long instructions in markdown more complex if we want + + Lorem markdownum recepta avidum, missa de quam patientia, antris: cum defuit, + Titan repetemus nomine, ignare. Quod ad aura, et non quod vidisse utque ulla: + + - Pro inposuit tibi orsa tum artes ferox + - Acmon plausu qua agrestum situs virgo in + - Vacuus a pendens rostro non si pharetrae + - Haeremusque quos auxiliaris coniunx + - Repulsa impediunt munera teneri fallebat + - Bracchia frustra telo Iovis faucibus casus + +exercises_payload: + options: + shuffle_exercises: false + shuffle_groups: false + + exercise_groups: + - group: + - instructions: + - locale: fr_FR + value: Touch le rond jaune + - locale: en_US + value: Touch the yellow circle + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: yellow + type: color + is_right_answer: true + - value: red + type: color + - value: green + type: color + - value: blue + type: color + + - instructions: + - locale: fr_FR + value: Touch les ronds verts et bleus + - locale: en_US + value: Touch the green and blue circles + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: yellow + type: color + - value: red + type: color + - value: green + type: color + is_right_answer: true + - value: blue + type: color + is_right_answer: true + + - group: + - instructions: + - locale: fr_FR + value: Touch la pastèque + - locale: en_US + value: Touch the watermelon + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: 🍉 + type: emoji + is_right_answer: true + - value: 🍌 + type: emoji + - value: 🍒 + type: emoji + - value: 🥝 + type: emoji + + - instructions: + - locale: fr_FR + value: Touch le chat et le chien + - locale: en_US + value: Touch the cat and the dog + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: 🐱 + type: emoji + is_right_answer: true + - value: 🐶 + type: emoji + is_right_answer: true + - value: 🐷 + type: emoji + - value: 🐹 + type: emoji + + - instructions: + - locale: fr_FR + value: Touch le marteau et la scie + - locale: en_US + value: Touch the hammer and the saw + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: 🔨 + type: emoji + is_right_answer: true + - value: 🪚 + type: emoji + is_right_answer: true + - value: 🪛 + type: emoji + - value: 🪜 + type: emoji + + - group: + - instructions: + - locale: fr_FR + value: Touch le carré + - locale: en_US + value: Touch the square + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: circle + type: sfsymbol + - value: square + type: sfsymbol + is_right_answer: true + - value: triangle + type: sfsymbol + - value: rhombus + type: sfsymbol + + - instructions: + - locale: fr_FR + value: Touch la personne jouant au tennis + - locale: en_US + value: Touch the person playing tennis + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: figure.climbing + type: sfsymbol + - value: figure.tennis + type: sfsymbol + is_right_answer: true + - value: figure.pool.swim + type: sfsymbol + - value: figure.soccer + type: sfsymbol + + - instructions: + - locale: fr_FR + value: Touch l'orage + - locale: en_US + value: Touch the storm + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: sun.max + type: sfsymbol + - value: cloud.sun + type: sfsymbol + - value: cloud.bolt.rain + type: sfsymbol + is_right_answer: true + - value: cloud.snow + type: sfsymbol + + - instructions: + - locale: fr_FR + value: Touch le vélo + - locale: en_US + value: Touch the bike + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: bicycle + type: sfsymbol + is_right_answer: true + - value: car + type: sfsymbol + - value: airplane + type: sfsymbol + - value: sailboat + type: sfsymbol diff --git a/Modules/ContentKit/Resources/Content/activities/templates/sample_2-6102794F02D3423482E243BCBC7F8CA8.activity.yml b/Modules/ContentKit/Resources/Content/activities/templates/sample_2-6102794F02D3423482E243BCBC7F8CA8.activity.yml new file mode 100644 index 0000000000..0285c2324a --- /dev/null +++ b/Modules/ContentKit/Resources/Content/activities/templates/sample_2-6102794F02D3423482E243BCBC7F8CA8.activity.yml @@ -0,0 +1,295 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 + +uuid: 6102794F02D3423482E243BCBC7F8CA8 +name: sample_2 + +created_at: "2024-02-28T14:49:34.312602" +last_edited_at: "2024-03-05T09:59:06.751849" +status: published + +authors: + - leka + - aurore_kiesler + - julie_tuil + +skills: + - spatial_understanding + - recognition/animals + - communication/non_verbal_communication/gestures + +tags: + - tag_one + - tag_two + - tag_three + +hmi: + - robot + - magic_cards + - tablet_robot + +types: + - one_on_one + - group + +locales: + - en_US + - fr_FR + +l10n: + - locale: fr_FR + details: + icon: sample_2 + + title: Activité d'exemple 2 + subtitle: pour le développement + + short_description: > + Nunc Corinthiaci anilem, rerum primo et ambos fata? Et Drya aliis abest, *adeunt + deprenduntur* vidi age: venit *cuncti*, aures. Sentit tale nubibus! + + description: | + Lorem **markdownum arduus salve** sermone quanto fuit: ait nunc placitas precor + **Mercurium novi**: terra nec. Nullo si auctor tenentis **verbis**; sed ore. + Nisi sermone procul, et diu turbantes, opem nitidis, aera ad esse Pico refugit + iugulum? Gradus non verbis humana **rursus aeternum vincetis** gaudet inpia + puro, est. + + instructions: | + ## Longues instructions en markdown plus complexe si on veut + + Lorem markdownum recepta avidum, missa de quam patientia, antris: cum defuit, + Titan repetemus nomine, ignare. Quod ad aura, et non quod vidisse utque ulla: + + - Pro inposuit tibi orsa tum artes ferox + - Acmon plausu qua agrestum situs virgo in + - Vacuus a pendens rostro non si pharetrae + - Haeremusque quos auxiliaris coniunx + - Repulsa impediunt munera teneri fallebat + - Bracchia frustra telo Iovis faucibus casus + + - locale: en_US + details: + icon: sample_2 + + title: Sample activity 2 + subtitle: for development + + short_description: > + Nunc Corinthiaci anilem, rerum primo et ambos fata? Et Drya aliis abest, *adeunt + deprenduntur* vidi age: venit *cuncti*, aures. Sentit tale nubibus! + + description: | + Lorem **markdownum arduus salve** sermone quanto fuit: ait nunc placitas precor + **Mercurium novi**: terra nec. Nullo si auctor tenentis **verbis**; sed ore. + Nisi sermone procul, et diu turbantes, opem nitidis, aera ad esse Pico refugit + iugulum? Gradus non verbis humana **rursus aeternum vincetis** gaudet inpia + puro, est. + + instructions: | + ## Long instructions in markdown more complex if we want + + Lorem markdownum recepta avidum, missa de quam patientia, antris: cum defuit, + Titan repetemus nomine, ignare. Quod ad aura, et non quod vidisse utque ulla: + + - Pro inposuit tibi orsa tum artes ferox + - Acmon plausu qua agrestum situs virgo in + - Vacuus a pendens rostro non si pharetrae + - Haeremusque quos auxiliaris coniunx + - Repulsa impediunt munera teneri fallebat + - Bracchia frustra telo Iovis faucibus casus + +exercises_payload: + options: + shuffle_exercises: true + shuffle_groups: false + + exercise_groups: + - group: + - instructions: + - locale: fr_FR + value: Touch le rond jaune + - locale: en_US + value: Touch the yellow circle + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: yellow + type: color + is_right_answer: true + - value: red + type: color + - value: green + type: color + - value: blue + type: color + + - instructions: + - locale: fr_FR + value: Touch les ronds verts et bleus + - locale: en_US + value: Touch the green and blue circles + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: yellow + type: color + - value: red + type: color + - value: green + type: color + is_right_answer: true + - value: blue + type: color + is_right_answer: true + + - group: + - instructions: + - locale: fr_FR + value: Touch la pastèque + - locale: en_US + value: Touch the watermelon + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: 🍉 + type: emoji + is_right_answer: true + - value: 🍌 + type: emoji + - value: 🍒 + type: emoji + - value: 🥝 + type: emoji + + - instructions: + - locale: fr_FR + value: Touch le chat et le chien + - locale: en_US + value: Touch the cat and the dog + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: 🐱 + type: emoji + is_right_answer: true + - value: 🐶 + type: emoji + is_right_answer: true + - value: 🐷 + type: emoji + - value: 🐹 + type: emoji + + - instructions: + - locale: fr_FR + value: Touch le marteau et la scie + - locale: en_US + value: Touch the hammer and the saw + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: 🔨 + type: emoji + is_right_answer: true + - value: 🪚 + type: emoji + is_right_answer: true + - value: 🪛 + type: emoji + - value: 🪜 + type: emoji + + - group: + - instructions: + - locale: fr_FR + value: Touch le carré + - locale: en_US + value: Touch the square + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: circle + type: sfsymbol + - value: square + type: sfsymbol + is_right_answer: true + - value: triangle + type: sfsymbol + - value: rhombus + type: sfsymbol + + - instructions: + - locale: fr_FR + value: Touch la personne jouant au tennis + - locale: en_US + value: Touch the person playing tennis + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: figure.climbing + type: sfsymbol + - value: figure.tennis + type: sfsymbol + is_right_answer: true + - value: figure.pool.swim + type: sfsymbol + - value: figure.soccer + type: sfsymbol + + - instructions: + - locale: fr_FR + value: Touch l'orage + - locale: en_US + value: Touch the storm + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: sun.max + type: sfsymbol + - value: cloud.sun + type: sfsymbol + - value: cloud.bolt.rain + type: sfsymbol + is_right_answer: true + - value: cloud.rain + type: sfsymbol + + - instructions: + - locale: fr_FR + value: Touch le vélo + - locale: en_US + value: Touch the bike + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: bicycle + type: sfsymbol + is_right_answer: true + - value: car + type: sfsymbol + - value: airplane + type: sfsymbol + - value: sailboat + type: sfsymbol diff --git a/Modules/ContentKit/Resources/Content/definitions/activity_types.yml b/Modules/ContentKit/Resources/Content/definitions/activity_types.yml new file mode 100644 index 0000000000..a54889438c --- /dev/null +++ b/Modules/ContentKit/Resources/Content/definitions/activity_types.yml @@ -0,0 +1,38 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 +list: + - id: one_on_one + l10n: + - locale: fr_FR + name: Activité en tête-à-tête + description: > + Une activité en tête-à-tête est un engagement personnalisé entre un accompagnant et une seule personne accompagnée, + conçu pour favoriser une relation étroite, une attention individuelle et des expériences d'apprentissage + ou de divertissement sur mesure. Ce type d'activité permet une interaction directe, un retour immédiat + et des activités spécifiquement adaptées aux intérêts, capacités et besoins de développement de la personne accompagnée. + - locale: en_US + name: One-on-one activity + description: > + A one-on-one activity is a personalized engagement between one caregiver and one care receiver, designed to foster + a close relationship, individual attention, and tailored learning or entertainment experiences. This type of activity + allows for direct interaction, immediate feedback, and activities specifically tailored to the interests, abilities, + and developmental needs of the care receiver. + + - id: group + l10n: + - locale: fr_FR + name: Activité en groupe + description: > + Une activité de groupe implique plusieurs personnes accommpagnées, supervisées par un accompagnant, s'engageant dans une tâche + ou un jeu partagé. Elle est conçue pour encourager l'interaction sociale, le travail d'équipe et la résolution de problèmes collective. + Les activités de groupe aident les personnes à apprendre à coopérer, partager et naviguer dans les dynamiques sociales tout en profitant + des avantages de perspectives diverses et d'un effort collaboratif. + - locale: en_US + name: Group activity + description: | + A group activity involves multiple care receivers, supervised by a caregiver, engaging in a shared task or game. It is designed to + encourage social interaction, teamwork, and collective problem-solving. Group activities help individuals learn to cooperate, share, and + navigate social dynamics while benefiting from diverse perspectives and collaborative effort. diff --git a/Modules/ContentKit/Resources/Content/definitions/authors.yml b/Modules/ContentKit/Resources/Content/definitions/authors.yml new file mode 100644 index 0000000000..ecd39d90cb --- /dev/null +++ b/Modules/ContentKit/Resources/Content/definitions/authors.yml @@ -0,0 +1,57 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 +list: + - id: aurore_kiesler + visible: true + name: Aurore Kiesler + website: https://www.autismes-aujourdhui.fr/ + email: aurore@leka.io + professions: + - special_educator + l10n: + - locale: fr_FR + description: | + Aurore Kiesler est la présidente et membre fondateur de l'association Autisme Aujourd'hui, qui mène des actions pour sensibiliser au sujet de l'autisme, notamment à Strasbourg. Elle est également impliquée dans des ateliers utilisant la robotique pour soutenir les enfants atteints de troubles du spectre autistique (TSA). + Après 10 ans au DASCA en tant qu'éducatrice spécialisée, elle a rejoint l'équipe Leka en tant d'Experte Leka et Responsable Clientèle. + C'est elle que vous devez contacter si vous avez la moindre question! + - locale: en_US + description: | + Aurore Kiesler is the president and founding member of the association Autisme Aujourd'hui, which conducts actions to raise awareness about autism, particularly in Strasbourg. She is also involved in workshops using robotics to support children with Autism Spectrum Disorder (ASD). + After 10 years at DASCA as a special educator, she joined the Leka team as our Leka Expert and Customer Manager. + She is the one you should contact if you have any questions! + + - id: julie_tuil + visible: true + name: Julie Tuil + website: https://www.julietuil.com/ + email: '' + professions: + - speech_language_therapist + - aba_therapist + - clinical_director + l10n: + - locale: fr_FR + description: | + Julie Tuil est une spécialiste de l'autisme en France avec plus de 20 ans d'expérience. Elle propose des formations et des programmes basés sur l'Analyse Appliquée du Comportement (ABA) pour soutenir les enfants autistes et leurs familles, en mettant l'accent sur l'amélioration de la communication et la gestion des comportements. + - locale: en_US + description: | + Julie Tuil is an autism specialist in France with over 20 years of experience. She offers training and programs based on Applied Behavior Analysis (ABA) to support autistic children and their families, focusing on enhancing communication and behavior management. + + - id: leka + visible: true + name: Leka + website: https://leka.io + email: hello@leka.io + professions: [] + l10n: + - locale: fr_FR + description: | + Leka est une entreprise qui développe un compagnon robotique éducatif destiné aux enfants atteints de troubles neurodéveloppementaux, tels que le trouble du spectre de l'autisme (TSA), les multiples handicaps et la trisomie. Ce robot vise à capter l'attention des enfants et à interagir avec eux pour favoriser leur curiosité et encourager des interactions fluides. + Leka a rejoint APF France handicap en 2019 pour continuer à développer des solutions innovantes pour les personnes en situation de handicap. + - locale: en_US + description: | + Leka is a company that develops an educational robotic companion for children with neurodevelopmental disorders, such as Autism Spectrum Disorder (ASD), multiple disabilities, and Down Syndrome. This robot aims to capture children's attention and interact with them to foster their curiosity and encourage smooth interactions. + Leka joined APF France handicap in 2019 to continue developing innovative solutions for people with disabilities. diff --git a/Modules/ContentKit/Resources/Content/definitions/hmi.yml b/Modules/ContentKit/Resources/Content/definitions/hmi.yml new file mode 100644 index 0000000000..409c6c52fe --- /dev/null +++ b/Modules/ContentKit/Resources/Content/definitions/hmi.yml @@ -0,0 +1,49 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 +list: + - id: robot + l10n: + - locale: fr_FR + name: Robot seul + description: | + L'utilisateur interagit directement avec le robot. + - locale: en_US + name: Robot alone + description: | + The user interacts directly with the robot. + + - id: magic_cards + l10n: + - locale: fr_FR + name: Robot et cartes magiques + description: | + L'utilisateur interagit avec le robot en utilisant les cartes magiques. + - locale: en_US + name: Robot and magic cards + description: | + The user interacts with the robot by using magic cards. + + - id: tablet_robot + l10n: + - locale: fr_FR + name: iPad et robot + description: | + L'utilisateur interagit avec l'iPad, le robot agit en tant que support pour observer avant de répondre. + - locale: en_US + name: iPad and robot + description: | + The user interacts with the iPad, and the robot serves as support for observation before providing answers. + + - id: tablet + l10n: + - locale: fr_FR + name: iPad (robot en renforçateur) + description: | + L'utilisateur interagit avec l'iPad, le robot agit en tant que renforçateur. + - locale: en_US + name: iPad (robot as reinforcer) + description: | + The user interacts with the iPad, and the robot serves as reinforcer. diff --git a/Modules/ContentKit/Resources/Content/definitions/skills.yml b/Modules/ContentKit/Resources/Content/definitions/skills.yml new file mode 100644 index 0000000000..1e4fa6088c --- /dev/null +++ b/Modules/ContentKit/Resources/Content/definitions/skills.yml @@ -0,0 +1,868 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +version: 1.0.0 +list: + - id: association + l10n: + - locale: en_US + name: Association + description: > + Creating logical connections between elements that may seem different + and finding relationships between them. + - locale: fr_FR + name: Association + description: > + Créer des liens logiques entre des éléments qui semblent différents et + trouver des relations entre eux. + subskills: + - id: association/matching + l10n: + - locale: en_US + name: Matching + description: > + Finding two or more things that are exactly the same or very + similar (for example, pairing two identical socks) + - locale: fr_FR + name: Apparier + description: > + Trouver deux ou plusieurs choses qui sont exactement les mêmes ou + très similaires, (par exemple, mettre ensemble deux chaussettes + identiques). + subskills: [] + + - id: association/sorting + l10n: + - locale: en_US + name: Sorting + description: > + Putting things into groups based on what they have in common (for + example, sorting fruits by color) + - locale: fr_FR + name: Trier + description: > + Mettre des choses en groupes selon ce qu'elles ont en commun, (par + exemple, trier des fruits par couleur) + subskills: [] + + - id: association/ordering + l10n: + - locale: en_US + name: Ordering + description: > + Arranging things in order (for example, arranging from smallest to + largest, in chronological order, or in alphabetical order). + - locale: fr_FR + name: Ordonner + description: > + Mettre des choses dans un ordre (par exemple, ordonner du plus + petit au plus grand, par ordre chronologique ou par ordre + alphabétique). + subskills: [] + + - id: attention + l10n: + - locale: en_US + name: Attention + description: > + Ability to focus and pay attention to something (for example, + listening to someone speak without being distracted by other things + around). + - locale: fr_FR + name: Attention + description: > + Capacité à se concentrer et à prêter attention à quelque chose (par + exemple, écouter quelqu'un qui parle sans être distrait par d'autres + choses autour). + subskills: + - id: attention/sustained_attention + l10n: + - locale: en_US + name: Sustained attention + description: > + Ability to stay focused on one thing for a long time. + - locale: fr_FR + name: Attention soutenue + description: > + Capacité à rester concentré sur une chose pendant longtemps. + subskills: [] + + - id: attention/transition + l10n: + - locale: en_US + name: Transition between two activities + description: > + Ability to refocus quickly after a change in activity. + - locale: fr_FR + name: Transition entre deux activités + description: > + Capacité à se reconcentrer rapidement après un changement + d'activité + subskills: [] + + - id: attention/joint_attention + l10n: + - locale: en_US + name: Joint attention + description: > + Ability to focus on the same thing as someone else (for example, + looking at an object that someone is showing or listening to a + story together). + - locale: fr_FR + name: Attention conjointe + description: > + Capacité à se concentrer sur la même chose que quelqu'un d'autre + (par exemple, regarder un objet que quelqu'un montre ou écouter + une histoire ensemble). + subskills: [] + + - id: communication + l10n: + - locale: en_US + name: Communication + description: > + Communication is the process of exchanging information between + individuals, involving the transmission and reception of messages. + - locale: fr_FR + name: Communication + description: > + La communication est le processus d'échange d'informations entre des + individus, impliquant la transmission et la réception de messages. + subskills: + - id: communication/verbal_communication + l10n: + - locale: en_US + name: Verbal communication + description: > + Use of spoken or written words to express or receive messages. + - locale: fr_FR + name: Communication verbale + description: > + Utilisation de mots, parlés ou écrits, pour exprimer ou recevoir + des messages. + subskills: + - id: communication/verbal_communication/receptive_language + l10n: + - locale: en_US + name: Receptive language + description: > + Understanding what people say or write + - locale: fr_FR + name: Langage receptif + description: > + Comprendre ce que les gens disent ou écrivent. + subskills: + - id: >- + communication/verbal_communication/receptive_language/oral_comprehension + l10n: + - locale: en_US + name: Oral comprehension + description: > + Understanding the words people say. + - locale: fr_FR + name: Compréhension orale + description: > + Comprendre les mots que les gens disent. + subskills: [] + + - id: >- + communication/verbal_communication/receptive_language/written_comprehension + l10n: + - locale: en_US + name: Written comprehension (reading) + description: > + Understanding written words in books or elsewhere. + - locale: fr_FR + name: Compréhension écrite (lecture) + description: > + Comprendre les mots écrits dans les livres ou ailleurs. + subskills: [] + + - id: communication/verbal_communication/expressive_language + l10n: + - locale: en_US + name: Expressive language + description: > + Ability to speak or write to express messages. + - locale: fr_FR + name: Langage expressif + description: > + Pouvoir parler ou écrire pour exprimer des messages. + subskills: + - id: >- + communication/verbal_communication/expressive_language/oral_expression + l10n: + - locale: en_US + name: Oral expression (speech) + description: > + Using words to express oneself. + - locale: fr_FR + name: Expression orale (parole) + description: > + Utiliser des mots pour s'exprimer. + subskills: [] + + - id: >- + communication/verbal_communication/expressive_language/written_expression + l10n: + - locale: en_US + name: Written comprehension (reading) + description: > + Writing words to express oneself. + - locale: fr_FR + name: Expression écrite (écriture) + description: > + Ecrire des mots pour s'exprimer. + subskills: [] + + - id: communication/augmentative_alternative_communication + l10n: + - locale: en_US + name: Augmentative and alternative communication + description: > + Alternative methods for communicating when verbal communication is + challenging or not possible. + - locale: fr_FR + name: Communication augmentative et alternative + description: > + Méthodes alternatives pour communiquer quand la communication + verbale est difficile ou n'est pas possible. + subskills: + - id: communication/augmentative_alternative_communication/sign_language + l10n: + - locale: en_US + name: Sign language + description: > + Ability to use sign language to communicate. This involves + making signs with hands and using facial expressions to + express ideas without speaking. + - locale: fr_FR + name: Langage des signes + description: > + Capacité à utiliser la langue des signes pour communiquer. + Cela implique de faire des signes avec les mains et d'utiliser + des expressions faciales pour exprimer des idées sans parler. + subskills: [] + + - id: communication/augmentative_alternative_communication/makaton + l10n: + - locale: en_US + name: Makaton + description: > + Ability to use makaton, a method that combines manual signs + and/or symbols and words. Speech is present and is + complemented by signs and symbols to reinforce communication. + - locale: fr_FR + name: Makaton + description: > + Capacité à utiliser le Makaton, une méthode qui combine à la + fois des signes manuels et/ou des pictogrammes et des mots. La + parole est bien présente et est complétée par des signes et + des symboles pour renforcer la communication. + subskills: [] + + - id: communication/augmentative_alternative_communication/pecs + l10n: + - locale: en_US + name: PECS (picture exchange communication system) + description: > + Ability to use PECS, a system where people communicate using + images (pictograms, symbols, written words, photos, drawings, + etc.). They show or exchange images to express themselves. + - locale: fr_FR + name: PECS (picture exchange communication system) + description: > + Capacité à utiliser le PECS, un système où les gens + communiquent en utilisant des images (pictogrammes, symboles, + mots écrits, photos, dessins, etc.). Ils montrent ou échangent + des images pour s'exprimer. + subskills: [] + + - id: communication/non_verbal_communication + l10n: + - locale: en_US + name: Non verbal communication + description: > + Means of communication that do not involve spoken or written + words. + - locale: fr_FR + name: Communication non verbale + description: > + Moyens de communication qui ne font pas usage de mots parlés ou + écrits. + subskills: + - id: communication/non_verbal_communication/gestures + l10n: + - locale: en_US + name: Gestures + description: > + Moving hands and arms to show things without speaking. + - locale: fr_FR + name: Gestes + description: > + Bouger les mains et les bras pour montrer des choses sans + parler. + subskills: [] + + - id: communication/non_verbal_communication/pointing + l10n: + - locale: en_US + name: Pointing + description: > + Using a finger to indicate something or someone without using + words. + - locale: fr_FR + name: Pointage + description: > + Utiliser un doigt pour montrer quelque chose ou quelqu'un sans + utiliser de mots. + subskills: [] + + - id: communication/non_verbal_communication/gaze + l10n: + - locale: en_US + name: Gaze + description: > + Looking at things or people with eyes to show what one thinks + or feels without saying words. + - locale: fr_FR + name: Regard + description: > + Regarder des choses ou des personnes avec les yeux pour + montrer ce qu'on pense ou ce qu'on ressent sans dire des mots. + subskills: [] + + - id: communication/non_verbal_communication/facial_expressions + l10n: + - locale: en_US + name: Facial expressions + description: > + Making different expressions with the face to show what one + thinks or feels. + - locale: fr_FR + name: Mimiques + description: > + Faire des expressions différentes avec son visage pour montrer + comment on se sent, comme sourire quand on est content. + subskills: [] + + - id: communication/non_verbal_communication/vocalizations + l10n: + - locale: en_US + name: Vocalizations + description: > + Making sounds that are not real words. + - locale: fr_FR + name: Vocalises + description: > + Faire des sons qui ne sont pas de vrais mots. + subskills: [] + + - id: communication/non_verbal_communication/smiles + l10n: + - locale: en_US + name: Smiles + description: > + To smile to express joy, approval, or any kind of pleasant + emotion. + - locale: fr_FR + name: Sourires + description: > + Faire des sourires pour manifester de la joie, de + l'approbation ou toute sorte d'émotion agréable + subskills: [] + + - id: counting + l10n: + - locale: en_US + name: Counting + description: > + Knowing how to say numbers in order (1,2,3, and so on) and being able + to count things to determine quantity (for example, counting how many + apples are in a basket). + - locale: fr_FR + name: Comptage numérique + description: > + Savoir dire les nombres dans l'ordre (1,2,3 et ainsi de suite) et + pouvoir compter des choses pour déterminer la quantité. (par exemple, + compter combien de pommes sont dans un panier). + subskills: [] + - id: discrimination + l10n: + - locale: en_US + name: Discrimination + description: > + Ability to see the difference between things (for example, knowing the + difference between an apple and an orange by looking at them or + hearing the difference between sounds, like the sound of a piano and a + guitar). + - locale: fr_FR + name: Discrimination + description: > + Capacité à voir la différence entre des choses (par exemple, savoir + quelle est la différence entre une pomme et une orange en les + regardant ou entendre la différence entre des sons, comme le son d'un + piano et celui d'une guitare). + subskills: [] + - id: empathy + l10n: + - locale: en_US + name: Empathy + description: > + Understanding and feeling what others feel (for example, if someone is + sad, empathy allows understanding why they are sad and being able to + put oneself in their place). + - locale: fr_FR + name: Empathie + description: > + Comprendre et ressentir ce que les autres personnes ressentent (par + exemple, si quelqu'un est triste, l'empathie permet de comprendre + pourquoi il est triste et d'être capable de se mettre à sa place). + subskills: [] + - id: familiarization_with_leka + l10n: + - locale: en_US + name: Familiarization with Leka + description: > + The skill of "familiarization with Leka" involves helping the + care receiver feel comfortable and confident with the Leka robot. + - locale: fr_FR + name: Familiarisation avec Leka + description: > + La compétence "familiarisation avec Leka" consiste à aider + la personne accompagnée à se sentir à l'aise et en confiance avec le robot + Leka. + subskills: [] + - id: fine_motor_skills + l10n: + - locale: en_US + name: Fine motor skills + description: > + Fine motor skills refer to the ability to use small muscles, + especially those in the hands and fingers, to perform precise and + delicate tasks. It includes actions like writing, drawing, buttoning a + shirt, handling small objects, or using tools like scissors or + pencils. + - locale: fr_FR + name: Motricité fine + description: > + La motricité fine se réfère à la capacité d'utiliser les petits + muscles, particulièrement ceux des mains et des doigts, pour effectuer + des tâches précises et délicates. Elle inclut des actions comme + écrire, dessiner, boutonner une chemise, manipuler de petits objets, + ou utiliser des outils comme des ciseaux ou des crayons. + subskills: [] + - id: generalization + l10n: + - locale: en_US + name: Generalization + description: > + Learning something in one situation and knowing how to apply it in + different situations (for example, learning that a pen is used for + writing and then knowing that all pens are used for writing, even if + they are different colors shapes). + - locale: fr_FR + name: Généralisation + description: > + Apprendre quelque chose dans une situation et savoir l'utiliser dans + d'autres situations différentes (par exemple, apprendre qu'un stylo + sert à écrire, et ensuite savoir que tous les stylos servent à écrire, + même s'ils sont de différentes couleurs ou formes). + subskills: [] + - id: gross_motor_skills + l10n: + - locale: en_US + name: Gross motor skills + description: > + Ability to use large muscles for movements such as walking, running, + jumping, and throwing things. + - locale: fr_FR + name: Motricité globale + description: > + Capacité à utiliser les grands muscles pour faire des mouvements tels + que marcher, courir, sauter et lancer des choses. + subskills: [] + - id: inhibition + l10n: + - locale: en_US + name: Inhibition + description: > + Ability to refrain from doing something, even if tempted to do so (for + example, not touching the Leka robot when it's not my turn, even if I + really want to). + - locale: fr_FR + name: Inhibition + description: > + Capacité à se retenir de faire quelque chose, même si on a envie de le + faire (par exemple, ne pas toucher le robot Leka quand ce n'est pas + mon tour, même si j'en ai très envie). + subskills: [] + - id: memory + l10n: + - locale: en_US + name: Memory + description: > + Ability to store, retain, and remember information. + - locale: fr_FR + name: Mémoire + description: > + Capacité de stocker, garder et se souvenir des informations. + subskills: + - id: memory/short_term_memory + l10n: + - locale: en_US + name: Short-term memory + description: > + Ability to hold and process a limited amount of information for a + short period, usually from a few seconds to a minute (for example, + remembering the order of a sequence of colors just observed). + - locale: fr_FR + name: Mémoire court-terme + description: > + Capacité de retenir et de traiter une quantité limitée + d'informations pour une courte période, généralement de quelques + secondes à une minute (par exemple, garder en tête l'ordre d'une + séquence de couleurs que l'on vient d'observer). + subskills: [] + + - id: memory/long_term_memory + l10n: + - locale: en_US + name: Long-term memory + description: > + Ability to store and retrieve information over a long period, + ranging from a few minutes to a lifetime. It includes personal + memories, acquired knowledge, skills, and experiences. + - locale: fr_FR + name: Mémoire long terme + description: > + Capacité de stocker et de récupérer des informations sur une + longue période, allant de quelques minutes à toute une vie. Elle + inclut des souvenirs personnels, des connaissances acquises, des + compétences, et des expériences. + subskills: [] + + - id: recognition + l10n: + - locale: en_US + name: Recognition + description: > + The ability to see and identify different things. + - locale: fr_FR + name: Reconnaissance + description: > + La capacité à voir et à identifier différentes choses. + subskills: + - id: recognition/colors + l10n: + - locale: en_US + name: Color recognition + description: > + The ability to see and identify different colors. + - locale: fr_FR + name: Reconnaissance des couleurs + description: > + La reconnaissance des couleurs est la capacité à voir et à identifier + différentes couleurs. + subskills: [] + + - id: recognition/food + l10n: + - locale: en_US + name: Food recognition + description: > + The ability to see and identify different kinds of food (fruits, vegetables, + cakes, etc). + - locale: fr_FR + name: Reonnaissance des aliments + description: > + La reconnaissance des aliments est la capacité à voir et à identifier + différents aliments (fruits, légumes, gâteaux, etc.) + subskills: [] + + - id: recognition/animals + l10n: + - locale: en_US + name: Animal recognition + description: > + The ability to see and identify different animals (dogs, cats, horses, + etc.). + - locale: fr_FR + name: Reconnaissance des animaux + description: > + La reconnaissance des animaux est la capacité à voir et à identifier + différents animaux (chiens, chats, chevaux, etc.). + subskills: [] + + - id: recognition/geometric_shapes + l10n: + - locale: en_US + name: Share recognition + description: > + The ability to see and identify different shapes (round, square, + triangle, rectangle, etc.). + - locale: fr_FR + name: Reconnaissance des formes + description: > + La reconnaissance des formes est la capacité à voir et à identifier + différentes formes (rond, carré, triangle, rectangle, etc.). + subskills: [] + + - id: recognition/numbers + l10n: + - locale: en_US + name: Recognition of numbers + description: > + The ability to see and identify numbers. + - locale: fr_FR + name: Reconnaissance des chiffres + description: > + La reconnaissance des chiffres est la capacité à voir et à identifier + des chiffres. + subskills: [] + + - id: recognition/alphabet_letters + l10n: + - locale: en_US + name: Recognition of alphabet letters + description: > + The ability to see and identify different letters of the alphabet. + - locale: fr_FR + name: Reconnaissance des lettres de l'alphabet + description: > + La reconnaissance des lettres de l'alphabet est la capacité à voir et + à identifier différentes lettres de l'alphabet. + subskills: [] + + - id: recognition/everyday_objects + l10n: + - locale: en_US + name: Recognition of everyday objects + description: > + The ability to see and identify different everyday objects (clothes, + toothbrushes, plates, etc.). + - locale: fr_FR + name: Reconnaissance des objets du quotidien + description: > + La reconnaissance d'objets du quotidien est la capacité à voir et à + identifier différents objets du quotidien (vêtements, brosse à dents, + assiettes, etc.). + subskills: [] + + - id: recognition/basic_physiological_recognition_needs + l10n: + - locale: en_US + name: Recognition of basic physiological needs + description: > + The ability to see and identify basic physiological needs (eating, + drinking, sleeping, etc.). + - locale: fr_FR + name: Reconnaissance des besoins physiologiques primaires + description: > + La reconnaissance des besoins physiologiques primaires est la capacité + à voir et à identifier les besoins physiologiques primaires (manger, + boire, dormir, etc.). + subskills: [] + + - id: recognition/weather_elements + l10n: + - locale: en_US + name: Recognition of weather elements + description: > + The recognition of weather elements is the ability to see and identify + weather elements (sun, rain, clouds, snow, etc.). + - locale: fr_FR + name: Reconnaissance des éléments de la météo + description: > + La reconnaissance des éléments de la météo est la capacité à voir et + à identifier les éléments de la météo (soleil, pluie, nuages, neige, + etc.). + subskills: [] + + - id: recognition/body_parts + l10n: + - locale: en_US + name: Recognition of body parts + description: > + The recognition of body parts is the ability to see and identify + different parts of the body. + - locale: fr_FR + name: Reconnaissance des parties du corps humain + description: > + La reconnaissance des parties du corps est la capacité à voir et à + identifier les parties du corps. + subskills: [] + + - id: recognition/basic_emotions + l10n: + - locale: en_US + name: Recognition of basic emotions + description: > + The recognition of basic emotions is the ability to see and identify + fundamental emotions (joy, fear, anger, sadness, disgust). + - locale: fr_FR + name: Reconnaissance des émotions de base + description: > + La reconnaissance des émotions de base est la capacité à voir et à + identifier les émotions de base (joie, peur, colère, tristesse, + dégoût). + subskills: [] + + - id: recognition/places + l10n: + - locale: en_US + name: Recognition of places + description: > + The recognition of places is the ability to see and identify + differents places (cinema, swimming pool, school, etc). + - locale: fr_FR + name: Reconnaissance des lieux + description: > + La reconnaissance des lieux est la capacité à voir et à + identifier différents lieux (cinéma, piscine, école,etc). + subskills: [] + + - id: recognition/professions + l10n: + - locale: en_US + name: Recognition of professions + description: > + The recognition of professions is the ability to see and identify + differents professions (doctor, teacher, firefighter, etc). + - locale: fr_FR + name: Reconnaissance des métiers + description: > + La reconnaissance des métiers est la capacité à voir et à + identifier différents métiers (médecin, professeur, pompier, etc.) + subskills: [] + + - id: recognition/family_members + l10n: + - locale: en_US + name: Recognition of family members + description: > + The recognition of family members is the ability to see and identify + the different family members (father, mother, grandfather, etc). + - locale: fr_FR + name: Reconnaissance des membres de la famille + description: > + La reconnaissance des membres de la famille est la capacité à voir et + à identifier les différents membres de la famille + (père, mère, grand-père, etc.) + subskills: [] + + - id: relationship_tablet_robot + l10n: + - locale: en_US + name: Relationship between tablet and robot + description: > + Understanding the causal link between tablet and robot. Understanding + that touching the tablet makes the Leka robot move and change. + - locale: fr_FR + name: Relation entre tablette et robot + description: > + Comprendre le lien de causalité entre tablette et robot. Comprendre + qu'en touchant la tablette, cela fait bouger ou changer le robot Leka. + subskills: [] + - id: self_regulation + l10n: + - locale: en_US + name: Self-regulation + description: > + Ability to control reactions in response to emotions. Learning to calm + down after a strong emotion or not react impulsively. + - locale: fr_FR + name: Autorégulation + description: > + Capacité à contrôler ses réactions en réponse à ses émotions. + Apprendre à se calmer après une émotion forte ou à ne pas réagir de + manière impulsive. + subskills: [] + - id: sensory_interaction + l10n: + - locale: en_US + name: Sensory interaction + description: > + Manifests when a care receiver plays with the robot and uses their senses + to + interact. This includes watching Leka's changing colors and lights, + listening to the sounds or music it produces, and touching and feeling + Leka by manipulating or playing with it. + - locale: fr_FR + name: Sensorialité + description: > + Se manifeste quand une personne accompagnée joue avec le robot et utilise + ses sens + pour interagir. Cela inclut regarder les couleurs et lumières + changeantes de Leka, écouter les sons ou musiques qu'il émet, et + toucher et ressentir Leka en le manipulant ou en jouant avec lui. + subskills: [] + - id: social_interactions + l10n: + - locale: en_US + name: Social interactions + description: > + Social interactions occur when people interact and mutually influence + each other's behaviors. When a person speaks, uses gestures, or + expresses emotions, it elicits a response from another person. This + response, in turn, can influence the initial action or reaction of the + first person. + - locale: fr_FR + name: Interactions sociales + description: > + Les interactions sociales se déroulent lorsque les personnes + interagissent et influencent mutuellement leurs comportements. + Lorsqu'une personne parle, utilise des gestes ou exprime des émotions, + cela provoque une réponse chez une autre personne. Cette réponse, à + son tour, peut influencer l'action ou la réaction initiale de la + première personne. + subskills: [] + - id: spatial_understanding + l10n: + - locale: en_US + name: Spatial understanding + description: > + Understanding and knowing how to position oneself in a place, see + where things are around, and how to move or navigate in that space. + - locale: fr_FR + name: Apprehension de l'espace + description: > + Comprendre et savoir comment se situer dans un lieu, voir où les + choses se trouvent autour de soi, et comment bouger ou se déplacer + dans cet espace. + subskills: [] + - id: time_and_temporality_understanding + l10n: + - locale: en_US + name: Time and temporality understanding + description: > + Understanding time and knowing when things happen (For example, + breakfast is in the morning and dinner is in the evening). This also + means knowing how long things last (For example, a minute is shorter + than an hour) and understanding the order of events (For example, + today is after yesterday and before tomorrow, spring is after winter + and before summer). + - locale: fr_FR + name: Apprehension du temps et de la temporalité + description: > + Comprendre le temps et savoir quand les choses arrivent (par exemple, + le petit-déjeuner a lieu le matin et le diner a lieu le soir). Cela + signifie aussi savoir combien de temps durent les choses (par exemple, + une minute est plus courte qu'une heure) et connaître l'ordre des + événements (par exemple, aujourd'hui est après hier et avant demain, + le printemps est après l'hiver et avant l'été). + subskills: [] + - id: turn_taking + l10n: + - locale: en_US + name: Turn taking + description: > + Ability to know when to speak, listen, or act in a social interaction, + respecting a predetermined order or sequence. + - locale: fr_FR + name: Tour de rôle + description: > + Capacité de savoir quand parler, écouter ou agir dans une interaction + sociale, en respectant un ordre ou une séquence préétablie. + subskills: [] diff --git a/Modules/ContentKit/Sources/Activity/Activity+Mock.swift b/Modules/ContentKit/Sources/Activity/Activity+Mock.swift new file mode 100644 index 0000000000..d9f7174f96 --- /dev/null +++ b/Modules/ContentKit/Sources/Activity/Activity+Mock.swift @@ -0,0 +1,194 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Yams + +public extension Activity { + static var mock: Activity { + let data = mockActivityYaml.data(using: .utf8)! + let activity = try! YAMLDecoder().decode(Activity.self, from: data) // swiftlint:disable:this force_try + return activity + } + + // swiftformat:disable all + private static let mockActivityYaml = """ + version: 1.0.0 + + uuid: E7EE9CA4B13B49AF96CD77A9DF90833B + name: mock_activity + + created_at: 2024-02-28T12:53:48+00:00 + last_edited_at: 2024-02-28T12:53:48+00:00 + + status: published + + authors: + - leka + - aurore_kiesler + - julie_tuil + + skills: + - spatial_understanding + - recognition/animals + - communication/non_verbal_communication/gestures + - communication/verbal_communication/receptive_language + - association + - association/matching + - association/sorting + - association/ordering + - attention + - attention/sustained_attention + - attention/transition + - attention/joint_attention + + tags: + - tag_one + - tag_two + - tag_three + + hmi: + - robot + - magic_cards + - tablet_robot + + types: + - one_on_one + - group + + locales: + - en_US + - fr_FR + + l10n: + - locale: fr_FR + details: + icon: name_of_the_activity-icon-fr_FR.svg + + title: Activité d'exemple + subtitle: pour le développement + + short_description: > + Nunc Corinthiaci anilem, rerum primo et ambos fata? Et Drya aliis abest, *adeunt + deprenduntur* vidi age: venit *cuncti*, aures. Sentit tale nubibus! + + description: | + Lorem **markdownum arduus salve** sermone quanto fuit: ait nunc placitas precor + **Mercurium novi**: terra nec. Nullo si auctor tenentis **verbis**; sed ore. + Nisi sermone procul, et diu turbantes, opem nitidis, aera ad esse Pico refugit + iugulum? Gradus non verbis humana **rursus aeternum vincetis** gaudet inpia + puro, est. + + instructions: | + ## Longues instructions en markdown plus complexe si on veut + + Lorem markdownum recepta avidum, missa de quam patientia, antris: cum defuit, + Titan repetemus nomine, ignare. Quod ad aura, et non quod vidisse utque ulla: + + - Pro inposuit tibi orsa tum artes ferox + - Acmon plausu qua agrestum situs virgo in + - Vacuus a pendens rostro non si pharetrae + - Haeremusque quos auxiliaris coniunx + - Repulsa impediunt munera teneri fallebat + - Bracchia frustra telo Iovis faucibus casus + + - locale: en_US + details: + icon: name_of_the_activity-icon-en_US.svg + + title: Sample activity + subtitle: for development + + short_description: > + Nunc Corinthiaci anilem, rerum primo et ambos fata? Et Drya aliis abest, *adeunt + deprenduntur* vidi age: venit *cuncti*, aures. Sentit tale nubibus! + + description: | + Lorem **markdownum arduus salve** sermone quanto fuit: ait nunc placitas precor + **Mercurium novi**: terra nec. Nullo si auctor tenentis **verbis**; sed ore. + Nisi sermone procul, et diu turbantes, opem nitidis, aera ad esse Pico refugit + iugulum? Gradus non verbis humana **rursus aeternum vincetis** gaudet inpia + puro, est. + + instructions: | + ## Long instructions in markdown more complex if we want + + Lorem markdownum recepta avidum, missa de quam patientia, antris: cum defuit, + Titan repetemus nomine, ignare. Quod ad aura, et non quod vidisse utque ulla: + + - Pro inposuit tibi orsa tum artes ferox + - Acmon plausu qua agrestum situs virgo in + - Vacuus a pendens rostro non si pharetrae + - Haeremusque quos auxiliaris coniunx + - Repulsa impediunt munera teneri fallebat + - Bracchia frustra telo Iovis faucibus casus + + exercises_payload: + options: + shuffle_exercises: false + shuffle_groups: false + + exercise_groups: + - group: + - instructions: + - locale: fr_FR + value: Touch la pastèque + - locale: en_US + value: Touch the watermelon + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: 🍉 + type: emoji + is_right_answer: true + - value: 🍌 + type: emoji + - value: 🍒 + type: emoji + - value: 🥝 + type: emoji + + - instructions: + - locale: fr_FR + value: Touch le carré + - locale: en_US + value: Touch the square + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: circle + type: sfsymbol + - value: square + type: sfsymbol + is_right_answer: true + - value: triangle + type: sfsymbol + - value: rhombus + type: sfsymbol + + - instructions: + - locale: fr_FR + value: Touch le rond jaune + - locale: en_US + value: Touch the yellow circle + interface: touchToSelect + gameplay: findTheRightAnswers + payload: + shuffle_choices: true + choices: + - value: yellow + type: color + is_right_answer: true + - value: red + type: color + - value: green + type: color + - value: blue + type: color + """ + // swiftformat:enable all +} diff --git a/Modules/ContentKit/Sources/Activity/Activity.swift b/Modules/ContentKit/Sources/Activity/Activity.swift new file mode 100644 index 0000000000..416305f6a6 --- /dev/null +++ b/Modules/ContentKit/Sources/Activity/Activity.swift @@ -0,0 +1,232 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import LocalizationKit +import UIKit +import Yams + +// MARK: - Activity + +// swiftlint:disable nesting + +public struct Activity: Decodable, Identifiable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.uuid = try container.decode(String.self, forKey: .uuid) + self.name = try container.decode(String.self, forKey: .name) + self.createdAt = try container.decode(Date.self, forKey: .createdAt) + self.lastEditedAt = try container.decode(Date.self, forKey: .lastEditedAt) + self.status = try container.decode(Status.self, forKey: .status) + + self.authors = try container.decode([String].self, forKey: .authors) + self.skills = try container.decode([String].self, forKey: .skills) + self.hmi = try container.decode([String].self, forKey: .hmi) + self.types = try container.decode([String].self, forKey: .types) + self.tags = try container.decode([String].self, forKey: .tags) + + let localeStrings = try container.decode([String].self, forKey: .locales) + self.locales = localeStrings.compactMap { Locale(identifier: $0) } + self.l10n = try container.decode([LocalizedDetails].self, forKey: .l10n) + + self.exercisePayload = try container.decode(ExercisesPayload.self, forKey: .exercicesPayload) + } + + // MARK: Public + + public let uuid: String + public let name: String + public let createdAt: Date + public let lastEditedAt: Date + public let status: Status + + public let authors: [String] // TODO: (@ladislas) - implement authors + public let skills: [String] // TODO: (@ladislas) - implement skills + public let hmi: [String] // TODO: (@ladislas) - implement hmi + public let types: [String] // TODO: (@ladislas) - implement types + public let tags: [String] // TODO: (@ladislas) - implement tags + + public let locales: [Locale] + public let l10n: [LocalizedDetails] + + public var exercisePayload: ExercisesPayload + + public var id: String { self.uuid } + public var languages: [Locale.LanguageCode] { self.locales.compactMap(\.language.languageCode) } + + public var details: Details { + self.details(in: LocalizationKit.l10n.language) + } + + public func details(in language: Locale.LanguageCode) -> Details { + guard let details = self.l10n.first(where: { $0.language == language })?.details else { + log.error("No details found for language \(language)") + fatalError("💥 No details found for language \(language)") + } + + return details + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case uuid + case name + case createdAt = "created_at" + case lastEditedAt = "last_edited_at" + case status + case authors + case skills + case hmi + case types + case tags + case locales + case l10n + case exercicesPayload = "exercises_payload" + } +} + +// MARK: Activity.Status + +public extension Activity { + enum Status: String, Decodable { + case draft + case published + } +} + +// MARK: Activity.LocalizedDetails + +public extension Activity { + struct LocalizedDetails: Decodable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let localeString = try container.decode(String.self, forKey: .locale) + self.locale = Locale(identifier: localeString) + + self.details = try container.decode(Activity.Details.self, forKey: .details) + } + + // MARK: Public + + public let locale: Locale + public let details: Details + + public var language: Locale.LanguageCode { self.locale.language.languageCode! } + + // MARK: Private + + private enum CodingKeys: CodingKey { + case locale + case details + } + } +} + +// MARK: Activity.Details + +public extension Activity { + struct Details: Decodable { + // MARK: Public + + public let icon: String + public let title: String + public let subtitle: String? + public let shortDescription: String + public let description: String + public let instructions: String + + public var iconImage: UIImage { + UIImage(named: "\(self.icon).activity.icon.png", in: .module, with: nil) + ?? UIImage(named: "placeholder.activity.icon.png", in: .module, with: nil)! + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case icon + case title + case subtitle + case shortDescription = "short_description" + case description + case instructions + } + } +} + +// MARK: Activity.ExercisesPayload + +public extension Activity { + struct ExercisesPayload: Decodable { + // MARK: Public + + public let options: Options + public var exerciseGroups: [ExerciseGroup] + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case options + case exerciseGroups = "exercise_groups" + } + } +} + +public extension Activity.ExercisesPayload { + struct Options: Decodable { + // MARK: Public + + public let shuffleExercises: Bool + public let shuffleGroups: Bool + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case shuffleExercises = "shuffle_exercises" + case shuffleGroups = "shuffle_groups" + } + } + + struct ExerciseGroup: Decodable { + // MARK: Lifecycle + + public init(exercises: [Exercise]) { + self.exercises = exercises + } + + // MARK: Public + + public let exercises: [Exercise] + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case exercises = "group" + } + } +} + +// MARK: - Activity + Hashable + +extension Activity: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } +} + +// MARK: - Activity + Equatable + +extension Activity: Equatable { + public static func == (lhs: Activity, rhs: Activity) -> Bool { + lhs.uuid == rhs.uuid + } +} + +// swiftlint:enable nesting diff --git a/Modules/ContentKit/Sources/ContentKit+Decode.swift b/Modules/ContentKit/Sources/ContentKit+Decode.swift new file mode 100644 index 0000000000..d8c2f1a38d --- /dev/null +++ b/Modules/ContentKit/Sources/ContentKit+Decode.swift @@ -0,0 +1,26 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import Yams + +public extension ContentKit { + // TODO(@ladislas): maybe return optional activity instead of fatalError + static func decodeActivity(_ filename: String) -> Activity { + do { + guard let file = Bundle.main.path(forResource: filename, ofType: "yml") else { + log.error("File not found: \(filename)") + fatalError("💥 File not found: \(filename)") + } + + let data = try String(contentsOfFile: file, encoding: .utf8) + let activity = try YAMLDecoder().decode(Activity.self, from: data) + + return activity + } catch { + log.error("Error decoding \(filename): \(error)") + fatalError("💥 Error decoding \(filename): \(error)") + } + } +} diff --git a/Modules/ContentKit/Sources/ContentKit.swift b/Modules/ContentKit/Sources/ContentKit.swift new file mode 100644 index 0000000000..e64f654d17 --- /dev/null +++ b/Modules/ContentKit/Sources/ContentKit.swift @@ -0,0 +1,47 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import LogKit +import Yams + +let log = LogKit.createLoggerFor(module: "ContentKit") + +// MARK: - ContentKit + +public enum ContentKit { + public static func listSampleActivities() -> [Activity]? { + let bundle = Bundle.module + let files = bundle.paths(forResourcesOfType: "activity.yml", inDirectory: nil) + + var activities: [Activity] = [] + + for file in files { + let data = try? String(contentsOfFile: file, encoding: .utf8) + + guard let data else { + log.error("Error reading file: \(file)") + continue + } + + let activity = try? YAMLDecoder().decode(Activity.self, from: data) + + guard let activity else { + log.error("Error decoding file: \(file)") + continue + } + + activities.append(activity) + } + + return activities.sorted { $0.name < $1.name } + } + + public static func listImagesPNG() -> [String] { + let bundle = Bundle.module + let files = bundle.paths(forResourcesOfType: "png", inDirectory: nil) + + return files + } +} diff --git a/Modules/ContentKit/Sources/Exercise/Exercice+DanceFreeze.swift b/Modules/ContentKit/Sources/Exercise/Exercice+DanceFreeze.swift new file mode 100644 index 0000000000..95a4283023 --- /dev/null +++ b/Modules/ContentKit/Sources/Exercise/Exercice+DanceFreeze.swift @@ -0,0 +1,33 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// MARK: - DanceFreeze + +public enum DanceFreeze { + public struct Payload: Codable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let audioRecordingSongs: [AudioRecording.Song] = try container.decode( + [AudioRecording.Song].self, forKey: .songs + ) + self.songs = audioRecordingSongs.map { AudioRecording($0) } + } + + // MARK: Public + + public let songs: [AudioRecording] + + public func encode(to _: Encoder) throws { + fatalError("Not implemented") + } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case songs + } + } +} diff --git a/Modules/ContentKit/Sources/Exercise/Exercice+MidiRecordingPlayer.swift b/Modules/ContentKit/Sources/Exercise/Exercice+MidiRecordingPlayer.swift new file mode 100644 index 0000000000..6a6225595e --- /dev/null +++ b/Modules/ContentKit/Sources/Exercise/Exercice+MidiRecordingPlayer.swift @@ -0,0 +1,29 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +public enum MidiRecordingPlayer { + public struct Payload: Codable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.instrument = try container.decode(String.self, forKey: .instrument) + + let midiRecordingSongs = try container.decode([MidiRecording.Song].self, forKey: .songs) + self.songs = midiRecordingSongs.map { MidiRecording($0) } + } + + // MARK: Public + + public let instrument: String + public let songs: [MidiRecording] + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case instrument + case songs + } + } +} diff --git a/Modules/ContentKit/Sources/Exercise/Exercice+MusicalInstrument.swift b/Modules/ContentKit/Sources/Exercise/Exercice+MusicalInstrument.swift new file mode 100644 index 0000000000..bb361db19a --- /dev/null +++ b/Modules/ContentKit/Sources/Exercise/Exercice+MusicalInstrument.swift @@ -0,0 +1,32 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftlint:disable nesting + +public enum MusicalInstrument { + public struct Payload: Codable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.instrument = try container.decode(String.self, forKey: .instrument) + self.scale = try container.decode(String.self, forKey: .scale) + } + + // MARK: Public + + public let instrument: String + public let scale: String + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case instrument + case scale + } + } +} + +// swiftlint:enable nesting diff --git a/Modules/ContentKit/Sources/Exercise/Exercise+Action.swift b/Modules/ContentKit/Sources/Exercise/Exercise+Action.swift new file mode 100644 index 0000000000..a1dc12cfc4 --- /dev/null +++ b/Modules/ContentKit/Sources/Exercise/Exercise+Action.swift @@ -0,0 +1,129 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +// swiftlint:disable nesting + +public extension Exercise { + enum Action: Codable { + case ipad(type: ActionType) + case robot(type: ActionType) + + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + let valueContainer = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .value) + switch type { + case "ipad": + let valueType = try valueContainer.decode(ValueType.self, forKey: .type) + switch valueType { + case .color: + let color = try valueContainer.decode(String.self, forKey: .value) + self = .ipad(type: .color(color)) + case .image: + let image = try valueContainer.decode(String.self, forKey: .value) + self = .ipad(type: .image(image)) + case .audio: + let audio = try valueContainer.decode(String.self, forKey: .value) + self = .ipad(type: .audio(audio)) + case .speech: + let speech = try valueContainer.decode(String.self, forKey: .value) + self = .ipad(type: .speech(speech)) + } + case "robot": + let valueType = try valueContainer.decode(ValueType.self, forKey: .type) + switch valueType { + case .image: + let image = try valueContainer.decode(String.self, forKey: .value) + self = .robot(type: .image(image)) + case .color: + let color = try valueContainer.decode(String.self, forKey: .value) + self = .robot(type: .color(color)) + default: + throw DecodingError.dataCorruptedError( + forKey: .type, + in: valueContainer, + debugDescription: "Unexpected type for RobotMedia" + ) + } + default: + throw DecodingError.dataCorruptedError( + forKey: .type, + in: container, + debugDescription: + "Cannot decode ExercisePayload. Available keys: \(container.allKeys.map(\.stringValue))" + ) + } + } + + // MARK: Public + + public enum ActionType: Codable { + case color(String) + case image(String) + case audio(String) + case speech(String) + } + + public enum ValueType: String, Codable { + case color + case image + case audio + case speech + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case let .ipad(ipadAction): + try container.encode("ipad", forKey: .type) + var valueContainer = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .value) + switch ipadAction { + case let .color(value): + try valueContainer.encode("color", forKey: .type) + try valueContainer.encode(value, forKey: .value) + case let .image(name): + try valueContainer.encode("image", forKey: .type) + try valueContainer.encode(name, forKey: .value) + case let .audio(name): + try valueContainer.encode("audio", forKey: .type) + try valueContainer.encode(name, forKey: .value) + case let .speech(value): + try valueContainer.encode("speech", forKey: .type) + try valueContainer.encode(value, forKey: .value) + } + case let .robot(robotAction): + try container.encode("robot", forKey: .type) + var valueContainer = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .value) + switch robotAction { + case let .image(id): + try valueContainer.encode("image", forKey: .type) + try valueContainer.encode(id, forKey: .value) + case let .color(value): + try valueContainer.encode("color", forKey: .type) + try valueContainer.encode(value, forKey: .value) + case .audio: + log.error("Action Audio not available for robot ") + fatalError("💥 Action Audio not available for robot") + case .speech: + log.error("Action Speech not available for robot ") + fatalError("💥 Action Speech not available for robot") + } + } + } + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case type + case value + } + } +} + +// swiftlint:enable nesting diff --git a/Modules/ContentKit/Sources/Exercise/Exercise+DragAndDropIntoZones.swift b/Modules/ContentKit/Sources/Exercise/Exercise+DragAndDropIntoZones.swift new file mode 100644 index 0000000000..d75fbb6515 --- /dev/null +++ b/Modules/ContentKit/Sources/Exercise/Exercise+DragAndDropIntoZones.swift @@ -0,0 +1,33 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftlint:disable nesting + +public enum DragAndDropIntoZones { + public enum DropZone: String, Codable { + case zoneA + case zoneB + + // MARK: Public + + public struct Details: Codable { + public let value: String + public let type: Exercise.UIElementType + } + } + + public struct Choice: Codable { + public let value: String + public let type: Exercise.UIElementType + public let dropZone: DropZone? + } + + public struct Payload: Codable { + public let dropZoneA: DropZone.Details + public let dropZoneB: DropZone.Details? + public let choices: [Choice] + } +} + +// swiftlint:enable nesting diff --git a/Modules/ContentKit/Sources/Exercise/Exercise+DragAndDropToAssociate.swift b/Modules/ContentKit/Sources/Exercise/Exercise+DragAndDropToAssociate.swift new file mode 100644 index 0000000000..5ec8050cc5 --- /dev/null +++ b/Modules/ContentKit/Sources/Exercise/Exercise+DragAndDropToAssociate.swift @@ -0,0 +1,69 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftlint:disable nesting + +public enum DragAndDropToAssociate { + public enum Category: String, Codable { + case catA + case catB + case catC + } + + public struct Choice: Codable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.value = try container.decode(String.self, forKey: .value) + self.type = try container.decode(Exercise.UIElementType.self, forKey: .type) + self.category = try container.decodeIfPresent(Category.self, forKey: .category) ?? .none + } + + public init(value: String, type: Exercise.UIElementType, category: Category) { + self.value = value + self.type = type + self.category = category + } + + // MARK: Public + + public let value: String + public let type: Exercise.UIElementType + public let category: Category? + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case value + case type + case category + } + } + + public struct Payload: Codable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.choices = try container.decode([Choice].self, forKey: .choices) + self.shuffleChoices = try container.decodeIfPresent(Bool.self, forKey: .shuffleChoices) ?? false + } + + // MARK: Public + + public let choices: [Choice] + public let shuffleChoices: Bool + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case choices + case shuffleChoices = "shuffle_choices" + } + } +} + +// swiftlint:enable nesting diff --git a/Modules/ContentKit/Sources/Exercise/Exercise+Gameplay.swift b/Modules/ContentKit/Sources/Exercise/Exercise+Gameplay.swift new file mode 100644 index 0000000000..8d8508fccd --- /dev/null +++ b/Modules/ContentKit/Sources/Exercise/Exercise+Gameplay.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +public extension Exercise { + enum Gameplay: String, Codable { + case findTheRightAnswers + case associateCategories + } +} diff --git a/Modules/ContentKit/Sources/Exercise/Exercise+Interface.swift b/Modules/ContentKit/Sources/Exercise/Exercise+Interface.swift new file mode 100644 index 0000000000..6742bf314a --- /dev/null +++ b/Modules/ContentKit/Sources/Exercise/Exercise+Interface.swift @@ -0,0 +1,23 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +public extension Exercise { + enum Interface: String, Codable { + case touchToSelect + case robotThenTouchToSelect + case listenThenTouchToSelect + case observeThenTouchToSelect + case dragAndDropIntoZones + case dragAndDropToAssociate + case danceFreeze + case remoteStandard + case remoteArrow + case hideAndSeek + case musicalInstruments + case melody + case pairing + } +} diff --git a/Modules/ContentKit/Sources/Exercise/Exercise+Payload.swift b/Modules/ContentKit/Sources/Exercise/Exercise+Payload.swift new file mode 100644 index 0000000000..fda3155bb7 --- /dev/null +++ b/Modules/ContentKit/Sources/Exercise/Exercise+Payload.swift @@ -0,0 +1,33 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +// MARK: - ExercisePayloadProtocol + +public protocol ExercisePayloadProtocol: Codable {} + +// MARK: - TouchToSelect.Payload + ExercisePayloadProtocol + +extension TouchToSelect.Payload: ExercisePayloadProtocol {} + +// MARK: - DragAndDropIntoZones.Payload + ExercisePayloadProtocol + +extension DragAndDropIntoZones.Payload: ExercisePayloadProtocol {} + +// MARK: - DragAndDropToAssociate.Payload + ExercisePayloadProtocol + +extension DragAndDropToAssociate.Payload: ExercisePayloadProtocol {} + +// MARK: - MidiRecordingPlayer.Payload + ExercisePayloadProtocol + +extension MidiRecordingPlayer.Payload: ExercisePayloadProtocol {} + +// MARK: - MusicalInstrument.Payload + ExercisePayloadProtocol + +extension MusicalInstrument.Payload: ExercisePayloadProtocol {} + +// MARK: - DanceFreeze.Payload + ExercisePayloadProtocol + +extension DanceFreeze.Payload: ExercisePayloadProtocol {} diff --git a/Modules/ContentKit/Sources/Exercise/Exercise+TouchToAssociate.swift b/Modules/ContentKit/Sources/Exercise/Exercise+TouchToAssociate.swift new file mode 100644 index 0000000000..6bc8ec1118 --- /dev/null +++ b/Modules/ContentKit/Sources/Exercise/Exercise+TouchToAssociate.swift @@ -0,0 +1,22 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// TODO(@ladislas): Add real implementation when needed +public enum TouchToAssociate { + public enum Category: String, Codable { + case catA + case catB + case catC + } + + public struct Choice: Codable { + public let value: String + public let type: Exercise.UIElementType + public let category: Category? + } + + public struct Payload: Codable { + public let choices: [Choice] + } +} diff --git a/Modules/ContentKit/Sources/Exercise/Exercise+TouchToSelect.swift b/Modules/ContentKit/Sources/Exercise/Exercise+TouchToSelect.swift new file mode 100644 index 0000000000..2430dd16fb --- /dev/null +++ b/Modules/ContentKit/Sources/Exercise/Exercise+TouchToSelect.swift @@ -0,0 +1,63 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftlint:disable nesting + +public enum TouchToSelect { + public struct Choice: Codable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.value = try container.decode(String.self, forKey: .value) + self.type = try container.decode(Exercise.UIElementType.self, forKey: .type) + self.isRightAnswer = try container.decodeIfPresent(Bool.self, forKey: .isRightAnswer) ?? false + } + + public init(value: String, type: Exercise.UIElementType, isRightAnswer: Bool = false) { + self.value = value + self.type = type + self.isRightAnswer = isRightAnswer + } + + // MARK: Public + + public let value: String + public let type: Exercise.UIElementType + public let isRightAnswer: Bool + + // MARK: Private + + private enum CodingKeys: String, CodingKey { + case value + case type + case isRightAnswer = "is_right_answer" + } + } + + public struct Payload: Codable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.choices = try container.decode([Choice].self, forKey: .choices) + + self.shuffleChoices = try container.decodeIfPresent(Bool.self, forKey: .shuffleChoices) ?? false + } + + // MARK: Public + + public let choices: [Choice] + public let shuffleChoices: Bool + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case choices + case shuffleChoices = "shuffle_choices" + } + } +} + +// swiftlint:enable nesting diff --git a/Modules/ContentKit/Sources/Exercise/Exercise+UIElement.swift b/Modules/ContentKit/Sources/Exercise/Exercise+UIElement.swift new file mode 100644 index 0000000000..114a863c51 --- /dev/null +++ b/Modules/ContentKit/Sources/Exercise/Exercise+UIElement.swift @@ -0,0 +1,15 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +public extension Exercise { + enum UIElementType: String, Codable { + case image + case text + case color + case sfsymbol + case emoji + } +} diff --git a/Modules/ContentKit/Sources/Exercise/Exercise.swift b/Modules/ContentKit/Sources/Exercise/Exercise.swift new file mode 100644 index 0000000000..af31f35524 --- /dev/null +++ b/Modules/ContentKit/Sources/Exercise/Exercise.swift @@ -0,0 +1,117 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import LocalizationKit +import LogKit + +// MARK: - Exercise + +public struct Exercise: Decodable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.interface = try container.decode(Interface.self, forKey: .interface) + self.gameplay = try container.decodeIfPresent(Gameplay.self, forKey: .gameplay) + self.action = try container.decodeIfPresent(Action.self, forKey: .action) + + self.localizedInstructions = try? container.decode([LocalizedInstructions].self, forKey: .localizedInstructions) + + log.debug("localizedInstructions: \(self.localizedInstructions)") + + if let localizedInstructions = self.localizedInstructions { + let availableLocales = localizedInstructions.map(\.locale) + log.debug("\(availableLocales)") + + let currentLocale = availableLocales.first(where: { + $0.language.languageCode == LocalizationKit.l10n.language + }) ?? Locale(identifier: "en_US") + + log.debug("Current Language: \(LocalizationKit.l10n.language)") + log.debug("currentLocale: \(currentLocale)") + + self.instructions = self.localizedInstructions?.first(where: { $0.locale == currentLocale })?.value + // log.debug("instructions: \(self.instructions)") + log.debug("Selected Instructions: \(String(describing: self.instructions))") + } else { + self.instructions = nil + } + + switch (self.interface, self.gameplay) { + case (.touchToSelect, .findTheRightAnswers), + (.listenThenTouchToSelect, .findTheRightAnswers), + (.observeThenTouchToSelect, .findTheRightAnswers), + (.robotThenTouchToSelect, .findTheRightAnswers): + self.payload = try container.decode(TouchToSelect.Payload.self, forKey: .payload) + + case (.dragAndDropIntoZones, .findTheRightAnswers): + self.payload = try container.decode(DragAndDropIntoZones.Payload.self, forKey: .payload) + + case (.dragAndDropToAssociate, .associateCategories): + self.payload = try container.decode(DragAndDropToAssociate.Payload.self, forKey: .payload) + + case (.danceFreeze, .none): + self.payload = try container.decode(DanceFreeze.Payload.self, forKey: .payload) + + case (.musicalInstruments, .none): + self.payload = try container.decode(MusicalInstrument.Payload.self, forKey: .payload) + + case (.melody, .none): + self.payload = try container.decode(MidiRecordingPlayer.Payload.self, forKey: .payload) + + case (.remoteStandard, .none), + (.remoteArrow, .none), + (.hideAndSeek, .none), + (.pairing, .none): + self.payload = nil + + default: + throw DecodingError.dataCorruptedError( + forKey: .payload, in: container, debugDescription: "Invalid combination of interface or gameplay" + ) + } + } + + // MARK: Public + + public let instructions: String? + public let interface: Interface + public let gameplay: Gameplay? + public let action: Action? + public let payload: ExercisePayloadProtocol? + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case localizedInstructions = "instructions" + case interface + case gameplay + case action + case payload + } + + // MARK: Private + + private let localizedInstructions: [LocalizedInstructions]? +} + +// MARK: Exercise.LocalizedInstructions + +public extension Exercise { + struct LocalizedInstructions: Codable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.locale = try Locale(identifier: container.decode(String.self, forKey: .locale)) + self.value = try container.decode(String.self, forKey: .value) + } + + // MARK: Internal + + let locale: Locale + let value: String + } +} diff --git a/Modules/ContentKit/Sources/Models/ActivityTypes.swift b/Modules/ContentKit/Sources/Models/ActivityTypes.swift new file mode 100644 index 0000000000..745c732a3a --- /dev/null +++ b/Modules/ContentKit/Sources/Models/ActivityTypes.swift @@ -0,0 +1,108 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import LocalizationKit +import LogKit +import Version +import Yams + +// MARK: - ActivityTypes + +public class ActivityTypes: Codable { + // MARK: Lifecycle + + private init() { + self.container = Self.loadTypes() + } + + // MARK: Public + + public static var list: [ActivityType] { + shared.container.list + } + + public static func type(id: String) -> ActivityType? { + self.list.first(where: { $0.id == id }) + } + + // MARK: Private + + private struct TypesContainer: Codable { + let list: [ActivityType] + } + + private static let shared: ActivityTypes = .init() + + private let container: TypesContainer + + private static func loadTypes() -> TypesContainer { + if let fileURL = Bundle.module.url(forResource: "activity_types", withExtension: "yml") { + do { + let yamlString = try String(contentsOf: fileURL, encoding: .utf8) + let container = try YAMLDecoder().decode(TypesContainer.self, from: yamlString) + return container + } catch { + log.error("Failed to read YAML file: \(error)") + return TypesContainer(list: []) + } + } else { + log.error("activity_types.yml not found") + return TypesContainer(list: []) + } + } +} + +// MARK: - ActivityType + +public struct ActivityType: Codable, Identifiable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(String.self, forKey: .id) + + self.l10n = try container.decode([ActivityType.Localization].self, forKey: .l10n) + + let availableLocales = self.l10n.map(\.locale) + + let currentLocale = availableLocales.first(where: { + $0.language.languageCode == LocalizationKit.l10n.language + }) ?? Locale(identifier: "en_US") + + self.name = self.l10n.first(where: { $0.locale == currentLocale })!.name + self.description = self.l10n.first(where: { $0.locale == currentLocale })!.description + } + + // MARK: Public + + public let id: String + public let name: String + public let description: String + + // MARK: Private + + private let l10n: [Localization] +} + +// MARK: ActivityType.Localization + +public extension ActivityType { + struct Localization: Codable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.locale = try Locale(identifier: container.decode(String.self, forKey: .locale)) + self.name = try container.decode(String.self, forKey: .name) + self.description = try container.decode(String.self, forKey: .description) + } + + // MARK: Internal + + let locale: Locale + let name: String + let description: String + } +} diff --git a/Modules/ContentKit/Sources/Models/Authors.swift b/Modules/ContentKit/Sources/Models/Authors.swift new file mode 100644 index 0000000000..d293347a85 --- /dev/null +++ b/Modules/ContentKit/Sources/Models/Authors.swift @@ -0,0 +1,114 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import LocalizationKit +import LogKit +import Version +import Yams + +// MARK: - Authors + +public class Authors: Codable { + // MARK: Lifecycle + + private init() { + self.container = Self.loadHMI() + } + + // MARK: Public + + public static var list: [Author] { + shared.container.list + } + + public static func hmi(id: String) -> Author? { + self.list.first(where: { $0.id == id }) + } + + // MARK: Private + + private struct AuthorsContainer: Codable { + let list: [Author] + } + + private static let shared: Authors = .init() + + private let container: AuthorsContainer + + private static func loadHMI() -> AuthorsContainer { + if let fileURL = Bundle.module.url(forResource: "authors", withExtension: "yml") { + do { + let yamlString = try String(contentsOf: fileURL, encoding: .utf8) + let container = try YAMLDecoder().decode(AuthorsContainer.self, from: yamlString) + return container + } catch { + log.error("Failed to read YAML file: \(error)") + return AuthorsContainer(list: []) + } + } else { + log.error("hmi.yml not found") + return AuthorsContainer(list: []) + } + } +} + +// MARK: - Author + +public struct Author: Codable, Identifiable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(String.self, forKey: .id) + self.visible = try container.decode(Bool.self, forKey: .visible) + self.name = try container.decode(String.self, forKey: .name) + self.website = try container.decode(String.self, forKey: .website) + self.email = try container.decode(String.self, forKey: .email) + self.professions = try container.decode([String].self, forKey: .professions) + + self.l10n = try container.decode([Author.Localization].self, forKey: .l10n) + + let availableLocales = self.l10n.map(\.locale) + + let currentLocale = availableLocales.first(where: { + $0.language.languageCode == LocalizationKit.l10n.language + }) ?? Locale(identifier: "en_US") + + self.description = self.l10n.first(where: { $0.locale == currentLocale })!.description + } + + // MARK: Public + + public let id: String + public let visible: Bool + public let name: String + public let website: String + public let email: String + public let professions: [String] + public let description: String + + // MARK: Private + + private let l10n: [Localization] +} + +// MARK: Author.Localization + +public extension Author { + struct Localization: Codable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.locale = try Locale(identifier: container.decode(String.self, forKey: .locale)) + self.description = try container.decode(String.self, forKey: .description) + } + + // MARK: Internal + + let locale: Locale + let description: String + } +} diff --git a/Modules/ContentKit/Sources/Models/HMI.swift b/Modules/ContentKit/Sources/Models/HMI.swift new file mode 100644 index 0000000000..4bfbd4faef --- /dev/null +++ b/Modules/ContentKit/Sources/Models/HMI.swift @@ -0,0 +1,108 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import LocalizationKit +import LogKit +import Version +import Yams + +// MARK: - HMI + +public class HMI: Codable { + // MARK: Lifecycle + + private init() { + self.container = Self.loadHMI() + } + + // MARK: Public + + public static var list: [HMIDetails] { + shared.container.list + } + + public static func hmi(id: String) -> HMIDetails? { + self.list.first(where: { $0.id == id }) + } + + // MARK: Private + + private struct HMIContainer: Codable { + let list: [HMIDetails] + } + + private static let shared: HMI = .init() + + private let container: HMIContainer + + private static func loadHMI() -> HMIContainer { + if let fileURL = Bundle.module.url(forResource: "hmi", withExtension: "yml") { + do { + let yamlString = try String(contentsOf: fileURL, encoding: .utf8) + let container = try YAMLDecoder().decode(HMIContainer.self, from: yamlString) + return container + } catch { + log.error("Failed to read YAML file: \(error)") + return HMIContainer(list: []) + } + } else { + log.error("hmi.yml not found") + return HMIContainer(list: []) + } + } +} + +// MARK: - HMIDetails + +public struct HMIDetails: Codable, Identifiable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(String.self, forKey: .id) + + self.l10n = try container.decode([HMIDetails.Localization].self, forKey: .l10n) + + let availableLocales = self.l10n.map(\.locale) + + let currentLocale = availableLocales.first(where: { + $0.language.languageCode == LocalizationKit.l10n.language + }) ?? Locale(identifier: "en_US") + + self.name = self.l10n.first(where: { $0.locale == currentLocale })!.name + self.description = self.l10n.first(where: { $0.locale == currentLocale })!.description + } + + // MARK: Public + + public let id: String + public let name: String + public let description: String + + // MARK: Private + + private let l10n: [Localization] +} + +// MARK: HMIDetails.Localization + +public extension HMIDetails { + struct Localization: Codable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.locale = try Locale(identifier: container.decode(String.self, forKey: .locale)) + self.name = try container.decode(String.self, forKey: .name) + self.description = try container.decode(String.self, forKey: .description) + } + + // MARK: Internal + + let locale: Locale + let name: String + let description: String + } +} diff --git a/Modules/ContentKit/Sources/Models/Skills.swift b/Modules/ContentKit/Sources/Models/Skills.swift new file mode 100644 index 0000000000..a8b46d4496 --- /dev/null +++ b/Modules/ContentKit/Sources/Models/Skills.swift @@ -0,0 +1,132 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import LocalizationKit +import LogKit +import Version +import Yams + +// MARK: - Skills + +public class Skills { + // MARK: Lifecycle + + private init() { + self.container = Self.loadSkills() + } + + // MARK: Public + + public static var list: [Skill] { + shared.getAllSkills() + } + + public static func skill(id: String) -> Skill? { + self.list.first(where: { $0.id == id }) + } + + // MARK: Private + + private struct SkillsContainer: Codable { + let list: [Skill] + } + + private static let shared: Skills = .init() + + private let container: SkillsContainer + + private static func loadSkills() -> SkillsContainer { + if let fileURL = Bundle.module.url(forResource: "skills", withExtension: "yml") { + do { + let yamlString = try String(contentsOf: fileURL, encoding: .utf8) + let container = try YAMLDecoder().decode(SkillsContainer.self, from: yamlString) + return container + } catch { + log.error("Failed to read YAML file: \(error)") + return SkillsContainer(list: []) + } + } else { + log.error("skills.yml not found") + return SkillsContainer(list: []) + } + } + + private func getAllSkills() -> [Skill] { + var allSkills: [Skill] = [] + + func getSubskills(for skill: Skill) -> [Skill] { + var allSubskills: [Skill] = [] + + for subskill in skill.subskills { + allSubskills.append(subskill) + allSubskills.append(contentsOf: getSubskills(for: subskill)) + } + + return allSubskills + } + + for skill in self.container.list { + allSkills.append(skill) + allSkills.append(contentsOf: getSubskills(for: skill)) + } + + return allSkills + } +} + +// MARK: - Skill + +public struct Skill: Codable, Identifiable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(String.self, forKey: .id) + + self.l10n = try container.decode([Skill.Localization].self, forKey: .l10n) + + let availableLocales = self.l10n.map(\.locale) + + let currentLocale = availableLocales.first(where: { + $0.language.languageCode == LocalizationKit.l10n.language + }) ?? Locale(identifier: "en_US") + + self.name = self.l10n.first(where: { $0.locale == currentLocale })!.name + self.description = self.l10n.first(where: { $0.locale == currentLocale })!.description + self.subskills = try container.decode([Skill].self, forKey: .subskills) + } + + // MARK: Public + + public let id: String + public let name: String + public let description: String + public let subskills: [Skill] + + // MARK: Private + + private let l10n: [Localization] +} + +// MARK: Skill.Localization + +public extension Skill { + struct Localization: Codable { + // MARK: Lifecycle + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.locale = try Locale(identifier: container.decode(String.self, forKey: .locale)) + self.name = try container.decode(String.self, forKey: .name) + self.description = try container.decode(String.self, forKey: .description) + } + + // MARK: Internal + + let locale: Locale + let name: String + let description: String + } +} diff --git a/Modules/ContentKit/Sources/Utils/AudioRecording.swift b/Modules/ContentKit/Sources/Utils/AudioRecording.swift new file mode 100644 index 0000000000..9587ddc1d0 --- /dev/null +++ b/Modules/ContentKit/Sources/Utils/AudioRecording.swift @@ -0,0 +1,55 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +public struct AudioRecording: Codable, Hashable, Equatable { + // MARK: Lifecycle + + public init(name: String, file: String) { + self.name = name + self.file = file + } + + public init(_ song: Song) { + self.name = song.details.name + self.file = song.details.file + } + + // MARK: Public + + public enum Song: String, Codable { + case earlyBird + case emptyPage + case gigglySquirrel + case handsOn + case happyDays + case inTheGame + case littleByLittle + + // MARK: Internal + + var details: (name: String, file: String) { + switch self { + case .earlyBird: + (name: "Early Bird", file: "Early_Bird") + case .emptyPage: + (name: "Empty Page", file: "Empty_Page") + case .gigglySquirrel: + (name: "Giggly Squirrel", file: "Giggly_Squirrel") + case .handsOn: + (name: "Hands on", file: "Hands_On") + case .happyDays: + (name: "Happy Days", file: "Happy_Days") + case .inTheGame: + (name: "In the game", file: "In_The_Game") + case .littleByLittle: + (name: "Little by Little", file: "Little_by_little") + } + } + } + + public let name: String + public let file: String +} diff --git a/Modules/ContentKit/Sources/Utils/MidiRecording.swift b/Modules/ContentKit/Sources/Utils/MidiRecording.swift new file mode 100644 index 0000000000..f5f19ed2c3 --- /dev/null +++ b/Modules/ContentKit/Sources/Utils/MidiRecording.swift @@ -0,0 +1,85 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +public struct MidiRecording: Codable, Hashable, Equatable { + // MARK: Lifecycle + + public init(name: String, file: String, scale: [UInt8]) { + self.name = name + self.file = file + self.scale = scale + } + + public init(_ song: Song) { + self.name = song.details.name + self.file = song.details.file + self.scale = song.scale + } + + // MARK: Public + + public enum Song: String, Codable { + case none + case underTheMoonlight + case aGreenMouse + case twinkleTwinkleLittleStar + case londonBridgeIsFallingDown + case ohTheCrocodiles + case happyBirthday + + // MARK: Internal + + var details: (name: String, file: String) { + switch self { + case .none: + (name: "", file: "") + case .underTheMoonlight: + ( + name: "Under The Moonlight", file: "Under_The_Moonlight" + ) + case .aGreenMouse: + (name: "A Green Mouse", file: "A_Green_Mouse") + case .twinkleTwinkleLittleStar: + ( + name: "Twinkle Twinkle Little Star", file: "Twinkle_Twinkle_Little_Star" + ) + case .londonBridgeIsFallingDown: + ( + name: "London Bridge Is Falling Down", file: "London_Bridge_Is_Falling_Down" + ) + case .ohTheCrocodiles: + ( + name: "Oh The Crocodiles", file: "Oh_The_Crocodiles" + ) + case .happyBirthday: + (name: "Happy Birthday", file: "Happy_Birthday") + } + } + + var scale: [UInt8] { + switch self { + case .none: + [] + case .underTheMoonlight: + [24, 26, 28, 29, 31, 33, 35, 36] + case .aGreenMouse: + [24, 26, 28, 29, 31, 33, 34, 36] + case .twinkleTwinkleLittleStar: + [24, 26, 28, 29, 31, 33, 35, 36] + case .londonBridgeIsFallingDown: + [24, 26, 28, 29, 31, 33, 35, 36] + case .ohTheCrocodiles: + [24, 28, 29, 31, 33, 34, 35, 36] + case .happyBirthday: + [24, 26, 28, 29, 31, 33, 34, 36] + } + } + } + + public let name: String + public let file: String + public let scale: [UInt8] +} diff --git a/Modules/ContentKit/Tests/ContentKit_Tests.swift b/Modules/ContentKit/Tests/ContentKit_Tests.swift new file mode 100644 index 0000000000..16db408874 --- /dev/null +++ b/Modules/ContentKit/Tests/ContentKit_Tests.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +final class ContentKit_Tests: XCTestCase { + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/Modules/CoreUI/Project.swift b/Modules/CoreUI/Project.swift deleted file mode 100644 index 62e061bdbd..0000000000 --- a/Modules/CoreUI/Project.swift +++ /dev/null @@ -1,11 +0,0 @@ -// Leka - LekaOS -// Copyright 2022 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -import ProjectDescription -import ProjectDescriptionHelpers - -// Creates our project using a helper function defined in ProjectDescriptionHelpers -let project = Project.module(name: "CoreUI", - platform: .iOS, - dependencies: []) diff --git a/Modules/CoreUI/Sources/ContentView.swift b/Modules/CoreUI/Sources/ContentView.swift deleted file mode 100644 index 47d0470f9b..0000000000 --- a/Modules/CoreUI/Sources/ContentView.swift +++ /dev/null @@ -1,31 +0,0 @@ -import SwiftUI - -public struct Hello: View { - - let name: String - let color: Color - - public init(_ name: String, in color: Color) { - self.name = name - self.color = color - } - - public var body: some View { - VStack { - Text("Hello, \(name)!") - .padding(50) - .background(color) - - Image(uiImage: CoreUIAsset.Assets.lekaLogo.image) - .resizable() - .scaledToFit() - .frame(width: 200.0) - } - } -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - Hello("World", in: .pink) - } -} diff --git a/Modules/CoreUI/Tests/CoreUITests.swift b/Modules/CoreUI/Tests/CoreUITests.swift deleted file mode 100644 index cf000cdca5..0000000000 --- a/Modules/CoreUI/Tests/CoreUITests.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation -import XCTest - -final class CoreUITests: XCTestCase { - func test_example() { - XCTAssertEqual("CoreUI", "CoreUI") - } -} diff --git a/Modules/DesignKit/Project.swift b/Modules/DesignKit/Project.swift new file mode 100644 index 0000000000..6067e304b1 --- /dev/null +++ b/Modules/DesignKit/Project.swift @@ -0,0 +1,15 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.module( + name: "DesignKit", + dependencies: [ + .external(name: "Lottie"), + ] +) diff --git a/Modules/DesignKit/Resources/Assets.xcassets/Contents.json b/Modules/DesignKit/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/DesignKit/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Assets.xcassets/leka-logo.imageset/Contents.json b/Modules/DesignKit/Resources/Assets.xcassets/leka-logo.imageset/Contents.json new file mode 100644 index 0000000000..2a1bebd321 --- /dev/null +++ b/Modules/DesignKit/Resources/Assets.xcassets/leka-logo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "leka-logo.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/CoreUI/Resources/Assets.xcassets/leka-logo.imageset/leka-logo.svg b/Modules/DesignKit/Resources/Assets.xcassets/leka-logo.imageset/leka-logo.svg similarity index 100% rename from Modules/CoreUI/Resources/Assets.xcassets/leka-logo.imageset/leka-logo.svg rename to Modules/DesignKit/Resources/Assets.xcassets/leka-logo.imageset/leka-logo.svg diff --git a/Modules/DesignKit/Resources/Assets.xcassets/leka-robot.imageset/Contents.json b/Modules/DesignKit/Resources/Assets.xcassets/leka-robot.imageset/Contents.json new file mode 100644 index 0000000000..1edc1a9a72 --- /dev/null +++ b/Modules/DesignKit/Resources/Assets.xcassets/leka-robot.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "leka-robot.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Assets.xcassets/leka-robot.imageset/leka-robot.svg b/Modules/DesignKit/Resources/Assets.xcassets/leka-robot.imageset/leka-robot.svg new file mode 100644 index 0000000000..716d617177 --- /dev/null +++ b/Modules/DesignKit/Resources/Assets.xcassets/leka-robot.imageset/leka-robot.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52cdb1388782407bc9c3e4abbb8e4880b6eedae21de094f96edf9c76989d45d1 +size 2585 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1a.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1a.imageset/Contents.json new file mode 100644 index 0000000000..b4da961cb4 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-1a.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1a.imageset/avatars_boy-1a.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1a.imageset/avatars_boy-1a.svg new file mode 100644 index 0000000000..41b43ce170 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1a.imageset/avatars_boy-1a.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:994b72d17f817aed0fdc94c560c1feea99b792bd171433bc4552fdd2e851295f +size 3562 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1b.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1b.imageset/Contents.json new file mode 100644 index 0000000000..15ca3af6df --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-1b.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1b.imageset/avatars_boy-1b.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1b.imageset/avatars_boy-1b.svg new file mode 100644 index 0000000000..460840899f --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1b.imageset/avatars_boy-1b.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1dde06045fdaaf540d6e3864f7ad4eae1256652cf939e8f208d1c72275ac3601 +size 3671 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1c-138.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1c-138.imageset/Contents.json new file mode 100644 index 0000000000..6741d8f3c0 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1c-138.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-1c-138.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1c-138.imageset/avatars_boy-1c-138.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1c-138.imageset/avatars_boy-1c-138.svg new file mode 100644 index 0000000000..c3c0fb5300 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1c-138.imageset/avatars_boy-1c-138.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:edb1117e455eadbfdb49816ca44f17a1fa6fddf15bc4fbabf81761fa3ab9a24d +size 1544 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1c-75.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1c-75.imageset/Contents.json new file mode 100644 index 0000000000..362e8f6867 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1c-75.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-1c-75.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1c-75.imageset/avatars_boy-1c-75.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1c-75.imageset/avatars_boy-1c-75.svg new file mode 100644 index 0000000000..e2a45973e1 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1c-75.imageset/avatars_boy-1c-75.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:542c89aa903c51d70734ad9f5b9868b339b0dd4d9bee08ced8073a5004106fd4 +size 3567 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1d-144.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1d-144.imageset/Contents.json new file mode 100644 index 0000000000..e39363c5a0 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1d-144.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-1d-144.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1d-144.imageset/avatars_boy-1d-144.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1d-144.imageset/avatars_boy-1d-144.svg new file mode 100644 index 0000000000..78c7b1246b --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1d-144.imageset/avatars_boy-1d-144.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:746ad7bef528a98623c67dede1eecca0d7e284f4b16a1d18b5cb4dd8014248c0 +size 1544 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1d-76.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1d-76.imageset/Contents.json new file mode 100644 index 0000000000..a63b690340 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1d-76.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-1d-76.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1d-76.imageset/avatars_boy-1d-76.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1d-76.imageset/avatars_boy-1d-76.svg new file mode 100644 index 0000000000..e575263069 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1d-76.imageset/avatars_boy-1d-76.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:376aa6e2b4042f4665f7cfcc551fdf3a325bf0d349c23cdec1278d5cab70292d +size 3668 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1e.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1e.imageset/Contents.json new file mode 100644 index 0000000000..b506389eed --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1e.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-1e.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1e.imageset/avatars_boy-1e.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1e.imageset/avatars_boy-1e.svg new file mode 100644 index 0000000000..f4f1b8453e --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1e.imageset/avatars_boy-1e.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:376f2b9899f1b7c9a1b26db10e87d537971e88b060c15a9e99cc6a64cc7f27ff +size 3588 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1f.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1f.imageset/Contents.json new file mode 100644 index 0000000000..5505be7ffb --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1f.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-1f.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1f.imageset/avatars_boy-1f.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1f.imageset/avatars_boy-1f.svg new file mode 100644 index 0000000000..922c6b79bc --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1f.imageset/avatars_boy-1f.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:411b444f46bb735a5599ea6ea6d893a4bcf000fc160d032b5218a4d872027e6a +size 3580 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1g.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1g.imageset/Contents.json new file mode 100644 index 0000000000..3874eb22d3 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1g.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-1g.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1g.imageset/avatars_boy-1g.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1g.imageset/avatars_boy-1g.svg new file mode 100644 index 0000000000..2624de7c23 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-1g.imageset/avatars_boy-1g.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d898fee22d19e18c0075426c6fb6cd55ec16e1d85268f9854af18c33fbd4d974 +size 3596 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2a.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2a.imageset/Contents.json new file mode 100644 index 0000000000..24184b2e3e --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-2a.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2a.imageset/avatars_boy-2a.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2a.imageset/avatars_boy-2a.svg new file mode 100644 index 0000000000..a3dd9d09fa --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2a.imageset/avatars_boy-2a.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04487f3430297f27b76644e20a2c1bd69f9a67559a11e40b14c74642a4626949 +size 4280 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2b.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2b.imageset/Contents.json new file mode 100644 index 0000000000..78bd0236e9 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-2b.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2b.imageset/avatars_boy-2b.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2b.imageset/avatars_boy-2b.svg new file mode 100644 index 0000000000..9ac9ba3d5b --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2b.imageset/avatars_boy-2b.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c63a7972066d7562b01bcc67459c6ae37b3b1c3725fa3917c0a4c2c910dad5e3 +size 4351 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2c.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2c.imageset/Contents.json new file mode 100644 index 0000000000..87766ab09e --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2c.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-2c.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2c.imageset/avatars_boy-2c.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2c.imageset/avatars_boy-2c.svg new file mode 100644 index 0000000000..d6c0647401 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2c.imageset/avatars_boy-2c.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9a015de06ebee7e6cb674bed8631f425a53a943e7b5a44c0f686aaf8ae42e24 +size 4296 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2d.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2d.imageset/Contents.json new file mode 100644 index 0000000000..f6300837fa --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-2d.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2d.imageset/avatars_boy-2d.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2d.imageset/avatars_boy-2d.svg new file mode 100644 index 0000000000..3b3bc0d881 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2d.imageset/avatars_boy-2d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bce7dae4786489a4ade68c1e61c05fdc4565bd1367a8aac5058dda7368883175 +size 4352 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2e-115.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2e-115.imageset/Contents.json new file mode 100644 index 0000000000..c589983fd0 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2e-115.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-2e-115.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2e-115.imageset/avatars_boy-2e-115.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2e-115.imageset/avatars_boy-2e-115.svg new file mode 100644 index 0000000000..62bc5e0355 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2e-115.imageset/avatars_boy-2e-115.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5716392813131335662074c815bd8fead678bd97e35be5afdcfbc77b6f1c8bb7 +size 4278 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2e-82.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2e-82.imageset/Contents.json new file mode 100644 index 0000000000..1cfbd0f154 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2e-82.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-2e-82.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2e-82.imageset/avatars_boy-2e-82.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2e-82.imageset/avatars_boy-2e-82.svg new file mode 100644 index 0000000000..0e62fc0aad --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2e-82.imageset/avatars_boy-2e-82.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5ea7b763794e14a1e4341e42b771a7c8edf0ccdd6f19c757d814f2fed5ca117 +size 4284 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2f.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2f.imageset/Contents.json new file mode 100644 index 0000000000..77bce1d2b7 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2f.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-2f.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2f.imageset/avatars_boy-2f.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2f.imageset/avatars_boy-2f.svg new file mode 100644 index 0000000000..9abfe6dd01 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2f.imageset/avatars_boy-2f.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2800689a7ab45a98f1f277a5fe8c5834260352f1233a2fb0d7aaf7e85075644f +size 4264 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2g.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2g.imageset/Contents.json new file mode 100644 index 0000000000..01c3d067a5 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2g.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-2g.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2g.imageset/avatars_boy-2g.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2g.imageset/avatars_boy-2g.svg new file mode 100644 index 0000000000..6d7b7ad505 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-2g.imageset/avatars_boy-2g.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73a52de6c1c358a3d3dfddf06c8ce37aef98dde6c079d557f02aefe9746d3e51 +size 3623 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3a.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3a.imageset/Contents.json new file mode 100644 index 0000000000..26b66125b3 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-3a.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3a.imageset/avatars_boy-3a.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3a.imageset/avatars_boy-3a.svg new file mode 100644 index 0000000000..37b4980b94 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3a.imageset/avatars_boy-3a.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:135ec1cfed78bbab2686d3f520065320993df1ba1a10f7ab9182b4bcff8920f0 +size 3026 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3b.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3b.imageset/Contents.json new file mode 100644 index 0000000000..838561e931 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-3b.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3b.imageset/avatars_boy-3b.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3b.imageset/avatars_boy-3b.svg new file mode 100644 index 0000000000..fe2acc80bc --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3b.imageset/avatars_boy-3b.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bceaf2357f999f75030b09049883418a53d3b5e4f9fb807a24e41a2098fc959d +size 3124 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3c.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3c.imageset/Contents.json new file mode 100644 index 0000000000..0af30e85b0 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3c.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-3c.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3c.imageset/avatars_boy-3c.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3c.imageset/avatars_boy-3c.svg new file mode 100644 index 0000000000..29861d8725 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3c.imageset/avatars_boy-3c.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a5cc6ec129acde7eef1ea8927c4088166ddfeb11e1f0598d3f9c119499627f3 +size 3120 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3d.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3d.imageset/Contents.json new file mode 100644 index 0000000000..e47dc59f90 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-3d.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3d.imageset/avatars_boy-3d.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3d.imageset/avatars_boy-3d.svg new file mode 100644 index 0000000000..b41f862dc7 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3d.imageset/avatars_boy-3d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:675d66c121e25776f3d01b863835ef94f97345c870b2f1b53ec1ae91652b456c +size 3033 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3e.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3e.imageset/Contents.json new file mode 100644 index 0000000000..51f9712ba4 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3e.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-3e.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3e.imageset/avatars_boy-3e.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3e.imageset/avatars_boy-3e.svg new file mode 100644 index 0000000000..57bb0a3837 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3e.imageset/avatars_boy-3e.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ee6bbe02e6fb4b987fc87c6feac60f19dd16e85c580cc5606ae85d0c5e785c1 +size 3021 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3f.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3f.imageset/Contents.json new file mode 100644 index 0000000000..5c886c9dbd --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3f.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-3f.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3f.imageset/avatars_boy-3f.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3f.imageset/avatars_boy-3f.svg new file mode 100644 index 0000000000..d9c10cd21d --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3f.imageset/avatars_boy-3f.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8deec7812989f8a466fc074c412c94060c536a56e7fe7da45c3ced51b0f92ef0 +size 3766 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3g.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3g.imageset/Contents.json new file mode 100644 index 0000000000..0898ca0a85 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3g.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-3g.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3g.imageset/avatars_boy-3g.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3g.imageset/avatars_boy-3g.svg new file mode 100644 index 0000000000..31b88ab6e1 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-3g.imageset/avatars_boy-3g.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2358a1ce6196432577abd3a9c37886d557e7522f885abb0507673e286fbaa8d3 +size 3052 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4a.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4a.imageset/Contents.json new file mode 100644 index 0000000000..546dff6157 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-4a.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4a.imageset/avatars_boy-4a.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4a.imageset/avatars_boy-4a.svg new file mode 100644 index 0000000000..6152e6904f --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4a.imageset/avatars_boy-4a.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34085bc9a2fa8484c3d83ab89c73639d8ca6b98f9a12235b7140dc71917590f6 +size 3029 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4b.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4b.imageset/Contents.json new file mode 100644 index 0000000000..8c60a12707 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-4b.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4b.imageset/avatars_boy-4b.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4b.imageset/avatars_boy-4b.svg new file mode 100644 index 0000000000..9da1a1d35a --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4b.imageset/avatars_boy-4b.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0799e4957d8cf4cbee75d360b246ea529b712ff7d6f2339bdd89504d58484121 +size 3212 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4c.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4c.imageset/Contents.json new file mode 100644 index 0000000000..ef47e4c829 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4c.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-4c.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4c.imageset/avatars_boy-4c.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4c.imageset/avatars_boy-4c.svg new file mode 100644 index 0000000000..1d4643c6fd --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4c.imageset/avatars_boy-4c.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8501c2ec6278b82d51ba475ecf90cd2029dbecc4c34438d89149173991690aa1 +size 3026 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4d.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4d.imageset/Contents.json new file mode 100644 index 0000000000..066a493094 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-4d.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4d.imageset/avatars_boy-4d.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4d.imageset/avatars_boy-4d.svg new file mode 100644 index 0000000000..04260078a2 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4d.imageset/avatars_boy-4d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:153dd376e85c1db2d6861a0d752a0078c3669fae589738fd52189e3ee834b5bc +size 2840 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4e.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4e.imageset/Contents.json new file mode 100644 index 0000000000..e4434a5369 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4e.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-4e.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4e.imageset/avatars_boy-4e.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4e.imageset/avatars_boy-4e.svg new file mode 100644 index 0000000000..88ff3d7ee8 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4e.imageset/avatars_boy-4e.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da4314be9d241ea6c16ab152a9d808263e2b3010b5c0e19d5a759f09ebe5d0cf +size 2838 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4f.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4f.imageset/Contents.json new file mode 100644 index 0000000000..c51a6bd0ed --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4f.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-4f.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4f.imageset/avatars_boy-4f.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4f.imageset/avatars_boy-4f.svg new file mode 100644 index 0000000000..fd8b9992f9 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4f.imageset/avatars_boy-4f.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:314f30b58aeed648739800d73b412117552450ea5b6205f9d3cfe54d02193093 +size 2641 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4g.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4g.imageset/Contents.json new file mode 100644 index 0000000000..b9d69d8ee0 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4g.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_boy-4g.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4g.imageset/avatars_boy-4g.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4g.imageset/avatars_boy-4g.svg new file mode 100644 index 0000000000..eaeadeddaf --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_boy-4g.imageset/avatars_boy-4g.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91addf5f592d00d7c77b7145af261f6342b7cc8ec1a9ce7e7590bccd43243419 +size 2645 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1a.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1a.imageset/Contents.json new file mode 100644 index 0000000000..1440d9d4e7 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-1a.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1a.imageset/avatars_girl-1a.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1a.imageset/avatars_girl-1a.svg new file mode 100644 index 0000000000..784a374918 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1a.imageset/avatars_girl-1a.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4772c5e6de19983e6fae9d8d07323ca085fb923695402b1f283e11a646ed432 +size 3093 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1b.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1b.imageset/Contents.json new file mode 100644 index 0000000000..3ceb36cbd7 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-1b.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1b.imageset/avatars_girl-1b.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1b.imageset/avatars_girl-1b.svg new file mode 100644 index 0000000000..6ee07539e6 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1b.imageset/avatars_girl-1b.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7aab45fd0d72cc00bbb44f76f5a3858cd5c94df101b961b11232374ed65b44f +size 3185 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1c.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1c.imageset/Contents.json new file mode 100644 index 0000000000..367b35cb56 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1c.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-1c.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1c.imageset/avatars_girl-1c.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1c.imageset/avatars_girl-1c.svg new file mode 100644 index 0000000000..1fb7a597cf --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1c.imageset/avatars_girl-1c.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b15550a3f3db09bd9501be4940c8747c7617a0ee64ce39d5686c9827edbbd1a9 +size 3189 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1d.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1d.imageset/Contents.json new file mode 100644 index 0000000000..0af0913143 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-1d.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1d.imageset/avatars_girl-1d.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1d.imageset/avatars_girl-1d.svg new file mode 100644 index 0000000000..cbbdf01890 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1d.imageset/avatars_girl-1d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d17a3b4957ed409661ebf30b12eace19616c883b7e47ee32d339612203f730ec +size 3093 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1e.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1e.imageset/Contents.json new file mode 100644 index 0000000000..89ed433e82 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1e.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-1e.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1e.imageset/avatars_girl-1e.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1e.imageset/avatars_girl-1e.svg new file mode 100644 index 0000000000..36cb645b03 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1e.imageset/avatars_girl-1e.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd1d51eb85e4e865a774e34dee2206a67bf7f5daab67100f83d84d81eaa740d7 +size 3183 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1f.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1f.imageset/Contents.json new file mode 100644 index 0000000000..fb715f768b --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1f.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-1f.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1f.imageset/avatars_girl-1f.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1f.imageset/avatars_girl-1f.svg new file mode 100644 index 0000000000..ff251f62b7 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-1f.imageset/avatars_girl-1f.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:868717a266ce8158c0a862308b28410347983f6f4e16f2f109e24294697c2719 +size 3184 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2a.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2a.imageset/Contents.json new file mode 100644 index 0000000000..37abff5579 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-2a.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2a.imageset/avatars_girl-2a.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2a.imageset/avatars_girl-2a.svg new file mode 100644 index 0000000000..1bcc0c29e2 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2a.imageset/avatars_girl-2a.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4286ff82b5593caa58991dac0cd86bc01d0dbc38232dd2350bfcf5f848a7f5bf +size 3949 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2b.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2b.imageset/Contents.json new file mode 100644 index 0000000000..e02b45abcb --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-2b.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2b.imageset/avatars_girl-2b.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2b.imageset/avatars_girl-2b.svg new file mode 100644 index 0000000000..d0e55c970a --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2b.imageset/avatars_girl-2b.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1583c5268f01772c6808b800622042ddfca94c6552f502f4f00df057273c9a10 +size 3949 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2c.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2c.imageset/Contents.json new file mode 100644 index 0000000000..fc4ef55449 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2c.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-2c.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2c.imageset/avatars_girl-2c.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2c.imageset/avatars_girl-2c.svg new file mode 100644 index 0000000000..be62d97de1 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2c.imageset/avatars_girl-2c.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50e2e074ace3a81e9c7c62df068842815b6c5ea34b0c1ea576f0a4d557a3f532 +size 3948 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2d.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2d.imageset/Contents.json new file mode 100644 index 0000000000..783a863827 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-2d.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2d.imageset/avatars_girl-2d.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2d.imageset/avatars_girl-2d.svg new file mode 100644 index 0000000000..031aa067a1 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2d.imageset/avatars_girl-2d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e6abca49d54239985f8303c527506b22e5f1e3dafd9f791fa3660a38b24d37d +size 3949 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2e.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2e.imageset/Contents.json new file mode 100644 index 0000000000..9e9663fc79 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2e.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-2e.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2e.imageset/avatars_girl-2e.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2e.imageset/avatars_girl-2e.svg new file mode 100644 index 0000000000..be3469d526 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2e.imageset/avatars_girl-2e.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54be397972f86ad6f75cf151a2cf51a6e621da4640f3f29c13f7515238481688 +size 3948 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2f.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2f.imageset/Contents.json new file mode 100644 index 0000000000..ce126c7a2f --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2f.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-2f.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2f.imageset/avatars_girl-2f.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2f.imageset/avatars_girl-2f.svg new file mode 100644 index 0000000000..a584a011af --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-2f.imageset/avatars_girl-2f.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c119855d9f1be4c1267e43f441d73bc138adc728492fb7ab7d69844e9320bf8 +size 3877 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3a.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3a.imageset/Contents.json new file mode 100644 index 0000000000..25ff2b5f22 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-3a.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3a.imageset/avatars_girl-3a.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3a.imageset/avatars_girl-3a.svg new file mode 100644 index 0000000000..b56a768a6a --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3a.imageset/avatars_girl-3a.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35f8d06443728debff3969cab34401dd95441d3f3dadff8beb5755611b638e4f +size 2851 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3b.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3b.imageset/Contents.json new file mode 100644 index 0000000000..bc39eaaed6 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-3b.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3b.imageset/avatars_girl-3b.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3b.imageset/avatars_girl-3b.svg new file mode 100644 index 0000000000..35ad5281f1 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3b.imageset/avatars_girl-3b.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a50efc2b2bc4db80e4e4ae850c1d109aa8bed8fd0bfdc37824344ce9a647b90e +size 2851 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3c.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3c.imageset/Contents.json new file mode 100644 index 0000000000..bc1c00ebdc --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3c.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-3c.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3c.imageset/avatars_girl-3c.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3c.imageset/avatars_girl-3c.svg new file mode 100644 index 0000000000..a5cc335ed3 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3c.imageset/avatars_girl-3c.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:900e0d5c79a99a91772179fe9a1231126de54d10f6d687f49e94e59964c1ea78 +size 2850 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3d.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3d.imageset/Contents.json new file mode 100644 index 0000000000..5508339fa4 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-3d.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3d.imageset/avatars_girl-3d.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3d.imageset/avatars_girl-3d.svg new file mode 100644 index 0000000000..006fd39a30 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3d.imageset/avatars_girl-3d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a33b459bf9ae27cb1e07164543a40ce1aaf8c9d806b4045b6138fe7d25ecdeee +size 2841 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3e-62.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3e-62.imageset/Contents.json new file mode 100644 index 0000000000..b78855061d --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3e-62.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-3e-62.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3e-62.imageset/avatars_girl-3e-62.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3e-62.imageset/avatars_girl-3e-62.svg new file mode 100644 index 0000000000..3b8dce3c53 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3e-62.imageset/avatars_girl-3e-62.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:862c114e2f7ff0d18d5a63246365cefe4d9ef80d58324c6ffb3c0fbabf1c0661 +size 2872 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3e-97.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3e-97.imageset/Contents.json new file mode 100644 index 0000000000..28418f607e --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3e-97.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-3e-97.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3e-97.imageset/avatars_girl-3e-97.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3e-97.imageset/avatars_girl-3e-97.svg new file mode 100644 index 0000000000..b87c1a7f22 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-3e-97.imageset/avatars_girl-3e-97.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35cf895be1143c1fcf7a918c5ba260fd5de8241fe07c9ce02d9ab06579f14c7a +size 2846 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4a.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4a.imageset/Contents.json new file mode 100644 index 0000000000..bf73911e2a --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-4a.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4a.imageset/avatars_girl-4a.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4a.imageset/avatars_girl-4a.svg new file mode 100644 index 0000000000..07f379764a --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4a.imageset/avatars_girl-4a.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce0b88ea1921bc2f1db306ca7294eb526f2aaea55157691bbc775f7330f1cace +size 3002 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4b.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4b.imageset/Contents.json new file mode 100644 index 0000000000..d5fc8d3247 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-4b.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4b.imageset/avatars_girl-4b.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4b.imageset/avatars_girl-4b.svg new file mode 100644 index 0000000000..f4dea40f3e --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4b.imageset/avatars_girl-4b.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ef622824186ffd97e3d69fb548575ee77b72acb94425e4f1fe605c11e11e4af +size 3042 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4c.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4c.imageset/Contents.json new file mode 100644 index 0000000000..013003114b --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4c.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-4c.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4c.imageset/avatars_girl-4c.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4c.imageset/avatars_girl-4c.svg new file mode 100644 index 0000000000..818816c145 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4c.imageset/avatars_girl-4c.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1f1003b0635b1be111f1a17e26d77b32b18366e252bfcf1c4f5244db5ff8996 +size 3133 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4d.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4d.imageset/Contents.json new file mode 100644 index 0000000000..901f5830ce --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-4d.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4d.imageset/avatars_girl-4d.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4d.imageset/avatars_girl-4d.svg new file mode 100644 index 0000000000..21a4f03359 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4d.imageset/avatars_girl-4d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d79b4508d3479a6b73736f28f8ef6052cbaf7108f807d31c5d1080961978eda +size 3136 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4e.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4e.imageset/Contents.json new file mode 100644 index 0000000000..b466370b89 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4e.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-4e.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4e.imageset/avatars_girl-4e.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4e.imageset/avatars_girl-4e.svg new file mode 100644 index 0000000000..66f53f0295 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-4e.imageset/avatars_girl-4e.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:386a0d2120cd5b5d73c4590e0cf1eda5990cdbe26f0d0cc59cb1a51dddfc9257 +size 3042 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5a.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5a.imageset/Contents.json new file mode 100644 index 0000000000..4bde57128e --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-5a.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5a.imageset/avatars_girl-5a.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5a.imageset/avatars_girl-5a.svg new file mode 100644 index 0000000000..7ec737f2e0 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5a.imageset/avatars_girl-5a.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6c97eb1a998a20c4b0d76b03417b799bb15eda5b3b66191d220cf0e8b67f785 +size 2918 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5b.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5b.imageset/Contents.json new file mode 100644 index 0000000000..06d96fca1f --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-5b.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5b.imageset/avatars_girl-5b.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5b.imageset/avatars_girl-5b.svg new file mode 100644 index 0000000000..b33d662159 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5b.imageset/avatars_girl-5b.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:046cc098368b384322b4fa7657107702edb7b2f8af235e0a36d2949882f48faf +size 2957 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5c.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5c.imageset/Contents.json new file mode 100644 index 0000000000..d74f8ddb6b --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5c.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-5c.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5c.imageset/avatars_girl-5c.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5c.imageset/avatars_girl-5c.svg new file mode 100644 index 0000000000..1a234b75bb --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5c.imageset/avatars_girl-5c.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f6fbd51f368a8a7feff9a0a80cd859550c863be92d7a72c2d3107a4f3a85625 +size 2736 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5d.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5d.imageset/Contents.json new file mode 100644 index 0000000000..33274d946b --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-5d.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5d.imageset/avatars_girl-5d.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5d.imageset/avatars_girl-5d.svg new file mode 100644 index 0000000000..7db96406e7 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5d.imageset/avatars_girl-5d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b1f24ded37c8e3110956ebd0443fb4832afdb3092c1a56c8cf194a05d8e3ce9 +size 2964 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5e.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5e.imageset/Contents.json new file mode 100644 index 0000000000..0e9d288a69 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5e.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_girl-5e.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5e.imageset/avatars_girl-5e.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5e.imageset/avatars_girl-5e.svg new file mode 100644 index 0000000000..4d6b5d0d67 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_girl-5e.imageset/avatars_girl-5e.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d364e219c599438aa24384e6425457464a0321e34386ab58e56279489f3539e +size 2973 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-1a.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-1a.imageset/Contents.json new file mode 100644 index 0000000000..ba51d757a1 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-1a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-boy-1a.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-1a.imageset/avatars_leka-boy-1a.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-1a.imageset/avatars_leka-boy-1a.svg new file mode 100644 index 0000000000..b2b8b696f5 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-1a.imageset/avatars_leka-boy-1a.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ed7ce4e41dd12d1e79b6157fc8522893d75ac62054762cb69216a86ff18326c +size 1544 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-1b.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-1b.imageset/Contents.json new file mode 100644 index 0000000000..470fade6d6 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-1b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-boy-1b.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-1b.imageset/avatars_leka-boy-1b.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-1b.imageset/avatars_leka-boy-1b.svg new file mode 100644 index 0000000000..294b3a9502 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-1b.imageset/avatars_leka-boy-1b.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4860e358f0816baec40c031140fe3afe7f739c47759865b74474766244af8674 +size 1544 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2a.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2a.imageset/Contents.json new file mode 100644 index 0000000000..1832407f40 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-boy-2a.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2a.imageset/avatars_leka-boy-2a.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2a.imageset/avatars_leka-boy-2a.svg new file mode 100644 index 0000000000..794aa8e243 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2a.imageset/avatars_leka-boy-2a.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89f471ea3b16a4799862992c5e86ddbb94df0b2116d5c255a10f845fcfada718 +size 2175 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2b.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2b.imageset/Contents.json new file mode 100644 index 0000000000..5073f6444d --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-boy-2b.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2b.imageset/avatars_leka-boy-2b.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2b.imageset/avatars_leka-boy-2b.svg new file mode 100644 index 0000000000..ac774ca93e --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2b.imageset/avatars_leka-boy-2b.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eaaca6581b082f73c83bc0e09ece0883d0836d313c5c75bfd174e6c2acc98030 +size 2175 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2c.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2c.imageset/Contents.json new file mode 100644 index 0000000000..976c4c0635 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2c.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-boy-2c.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2c.imageset/avatars_leka-boy-2c.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2c.imageset/avatars_leka-boy-2c.svg new file mode 100644 index 0000000000..3565cebd8f --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2c.imageset/avatars_leka-boy-2c.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cf01df7e9ec3c0d4566b44e692bbe275e0eec046e2b88af7c6b3981e9d9820f +size 2175 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2d.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2d.imageset/Contents.json new file mode 100644 index 0000000000..73e448b322 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-boy-2d.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2d.imageset/avatars_leka-boy-2d.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2d.imageset/avatars_leka-boy-2d.svg new file mode 100644 index 0000000000..d3507fb4f4 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-boy-2d.imageset/avatars_leka-boy-2d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37763e515eaebd449cfe6d40d7bbb3e5993cf947ebdfe729fd8796e66c4b1d81 +size 2175 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1a.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1a.imageset/Contents.json new file mode 100644 index 0000000000..f90778d79b --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-1a.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1a.imageset/avatars_leka-girl-1a.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1a.imageset/avatars_leka-girl-1a.svg new file mode 100644 index 0000000000..d2a9f4e39c --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1a.imageset/avatars_leka-girl-1a.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20b58800fb6ca28a99c95866b6138bbca4f1bd3cbe717bdf33f5e69349a3c0b9 +size 2502 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1b.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1b.imageset/Contents.json new file mode 100644 index 0000000000..d9504eca22 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-1b.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1b.imageset/avatars_leka-girl-1b.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1b.imageset/avatars_leka-girl-1b.svg new file mode 100644 index 0000000000..490c6d5c4e --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1b.imageset/avatars_leka-girl-1b.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0a0e5fff5f59b34f1824e9513ed6bbfd4b767ab7396e32bcb7a70b2094a468f +size 3050 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1c-118.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1c-118.imageset/Contents.json new file mode 100644 index 0000000000..87c7bc6aef --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1c-118.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-1c-118.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1c-118.imageset/avatars_leka-girl-1c-118.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1c-118.imageset/avatars_leka-girl-1c-118.svg new file mode 100644 index 0000000000..e8de215081 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1c-118.imageset/avatars_leka-girl-1c-118.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36f750656912378558153fd4978f49efcfa8e1f9f991959e2927f793c0adb18a +size 3624 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1c-119.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1c-119.imageset/Contents.json new file mode 100644 index 0000000000..ea8e429d20 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1c-119.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-1c-119.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1c-119.imageset/avatars_leka-girl-1c-119.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1c-119.imageset/avatars_leka-girl-1c-119.svg new file mode 100644 index 0000000000..9fa662b449 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-1c-119.imageset/avatars_leka-girl-1c-119.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f089c320b28334c7383fe8e52232c2ee6e633805d49dcad08585bd0884443215 +size 3891 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2a.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2a.imageset/Contents.json new file mode 100644 index 0000000000..5aaa564777 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-2a.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2a.imageset/avatars_leka-girl-2a.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2a.imageset/avatars_leka-girl-2a.svg new file mode 100644 index 0000000000..bfe510bb70 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2a.imageset/avatars_leka-girl-2a.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f496ce1fd6a6ba473baad279ca12b175f3e6664018b685adc09e89ebb2acbbde +size 1960 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2b.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2b.imageset/Contents.json new file mode 100644 index 0000000000..38ddc34f71 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-2b.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2b.imageset/avatars_leka-girl-2b.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2b.imageset/avatars_leka-girl-2b.svg new file mode 100644 index 0000000000..ad0e1af8ad --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2b.imageset/avatars_leka-girl-2b.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa61cf561cd7b5cc0519803912e53eddb3c05ce67dd7ed82344740758023c4e8 +size 2152 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2c.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2c.imageset/Contents.json new file mode 100644 index 0000000000..074754353e --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2c.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-2c.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2c.imageset/avatars_leka-girl-2c.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2c.imageset/avatars_leka-girl-2c.svg new file mode 100644 index 0000000000..090c15aaa6 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2c.imageset/avatars_leka-girl-2c.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72e7ccf488cec0683b94a8a46c8ceb546333301933c34a6272cc4b9652544f7d +size 2332 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2d.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2d.imageset/Contents.json new file mode 100644 index 0000000000..3b88f6ee81 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-2d.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2d.imageset/avatars_leka-girl-2d.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2d.imageset/avatars_leka-girl-2d.svg new file mode 100644 index 0000000000..ec96e60a3b --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-2d.imageset/avatars_leka-girl-2d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f25eede9e8c39a5831047b0682925d8bf05d9c493f92aed372a5c313ea4f7cf +size 2248 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3a.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3a.imageset/Contents.json new file mode 100644 index 0000000000..69deb10c8f --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-3a.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3a.imageset/avatars_leka-girl-3a.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3a.imageset/avatars_leka-girl-3a.svg new file mode 100644 index 0000000000..93e3270e5a --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3a.imageset/avatars_leka-girl-3a.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca56eac81fcd70f915d1f361cbfd8780bd98bcc94353381528815a942e6c364c +size 2997 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3b.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3b.imageset/Contents.json new file mode 100644 index 0000000000..972f0966b1 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-3b.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3b.imageset/avatars_leka-girl-3b.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3b.imageset/avatars_leka-girl-3b.svg new file mode 100644 index 0000000000..d7f2fa2ee5 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3b.imageset/avatars_leka-girl-3b.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53e9a37084c19ae4a892d1e9dd258ec36250ef0ec033e8c2072e879da0ea35ec +size 3003 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3c.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3c.imageset/Contents.json new file mode 100644 index 0000000000..e4c6eab1af --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3c.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-3c.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3c.imageset/avatars_leka-girl-3c.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3c.imageset/avatars_leka-girl-3c.svg new file mode 100644 index 0000000000..edfb25f652 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3c.imageset/avatars_leka-girl-3c.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4638d728136b90cd0ad7258a380dad35f8932d6c79e91f66f1213a0a096d587e +size 3003 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3d.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3d.imageset/Contents.json new file mode 100644 index 0000000000..43e016a165 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-3d.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3d.imageset/avatars_leka-girl-3d.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3d.imageset/avatars_leka-girl-3d.svg new file mode 100644 index 0000000000..cf0f9e6181 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-3d.imageset/avatars_leka-girl-3d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2bf73d3cba30066505226f272311e88a5dfe4ac59a57a8f79a3a15c61767f379 +size 2997 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4a.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4a.imageset/Contents.json new file mode 100644 index 0000000000..6a5378a12e --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-4a.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4a.imageset/avatars_leka-girl-4a.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4a.imageset/avatars_leka-girl-4a.svg new file mode 100644 index 0000000000..81d354e941 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4a.imageset/avatars_leka-girl-4a.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3436291c653b4d1d8f99fff4e205f088207cc73324b6bc09f04a9cd61b116b6 +size 3480 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4b.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4b.imageset/Contents.json new file mode 100644 index 0000000000..d6af5c6e53 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-4b.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4b.imageset/avatars_leka-girl-4b.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4b.imageset/avatars_leka-girl-4b.svg new file mode 100644 index 0000000000..7d1481384c --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4b.imageset/avatars_leka-girl-4b.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc2c39082f9d5c16056cab24798a4b78661ca08597912cffeccb91218c1a6069 +size 3484 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4c.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4c.imageset/Contents.json new file mode 100644 index 0000000000..ffd003b4e9 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4c.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-4c.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4c.imageset/avatars_leka-girl-4c.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4c.imageset/avatars_leka-girl-4c.svg new file mode 100644 index 0000000000..18526a1d98 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4c.imageset/avatars_leka-girl-4c.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18da7c06a447d858f40514109eafb927eebd93e2147a8c4a513d53d22f7cfe31 +size 3484 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4d.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4d.imageset/Contents.json new file mode 100644 index 0000000000..77d5ebf81e --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-4d.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4d.imageset/avatars_leka-girl-4d.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4d.imageset/avatars_leka-girl-4d.svg new file mode 100644 index 0000000000..db7448fa5b --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-4d.imageset/avatars_leka-girl-4d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57c8b8a21b42045b11706bcd3074f24a3dc121ae4d9d2b2bcb9cba3f3a8798be +size 3480 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5a.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5a.imageset/Contents.json new file mode 100644 index 0000000000..d48f0f9abe --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-5a.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5a.imageset/avatars_leka-girl-5a.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5a.imageset/avatars_leka-girl-5a.svg new file mode 100644 index 0000000000..f459d0e5d5 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5a.imageset/avatars_leka-girl-5a.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05cfaf3726b5b44c91897fc14bf2fa7e50c88940236839b5cedeb5b489341ef7 +size 1973 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5b.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5b.imageset/Contents.json new file mode 100644 index 0000000000..be259702ae --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-5b.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5b.imageset/avatars_leka-girl-5b.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5b.imageset/avatars_leka-girl-5b.svg new file mode 100644 index 0000000000..fa91154493 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5b.imageset/avatars_leka-girl-5b.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e1e05a7d62e4de6c5c68ca4c11247e2a090bad93c5a0338895dde11683cf21c +size 1973 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5c.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5c.imageset/Contents.json new file mode 100644 index 0000000000..9021dff566 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5c.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-5c.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5c.imageset/avatars_leka-girl-5c.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5c.imageset/avatars_leka-girl-5c.svg new file mode 100644 index 0000000000..f9e49bf8f5 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5c.imageset/avatars_leka-girl-5c.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42ba9baf05231124e0f2df7ae671016d404f8177623aad01ed5642e957309924 +size 1962 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5d.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5d.imageset/Contents.json new file mode 100644 index 0000000000..9ee35ec392 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-5d.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5d.imageset/avatars_leka-girl-5d.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5d.imageset/avatars_leka-girl-5d.svg new file mode 100644 index 0000000000..8df5de73ad --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-5d.imageset/avatars_leka-girl-5d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9983e143209b01c865fc81d887e63a2f1928db7c77c190c2d8d7685148d3f897 +size 1961 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6a.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6a.imageset/Contents.json new file mode 100644 index 0000000000..f55e17feaf --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6a.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-6a.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6a.imageset/avatars_leka-girl-6a.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6a.imageset/avatars_leka-girl-6a.svg new file mode 100644 index 0000000000..5587e3d21f --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6a.imageset/avatars_leka-girl-6a.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c871ab909704d2a3d5c0f52a72df1a73413e39a91fe19725690e3c3e598332e6 +size 2160 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6b.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6b.imageset/Contents.json new file mode 100644 index 0000000000..c4122a852d --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6b.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-6b.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6b.imageset/avatars_leka-girl-6b.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6b.imageset/avatars_leka-girl-6b.svg new file mode 100644 index 0000000000..2a6c398095 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6b.imageset/avatars_leka-girl-6b.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f69eef0acddf5aba1984a30601baffeeb09223c151b03441fd1eebc365a4d2ca +size 2161 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6c.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6c.imageset/Contents.json new file mode 100644 index 0000000000..267a45f377 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6c.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-6c.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6c.imageset/avatars_leka-girl-6c.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6c.imageset/avatars_leka-girl-6c.svg new file mode 100644 index 0000000000..e41c18e641 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6c.imageset/avatars_leka-girl-6c.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:559d0d516dfd2f824b636a650b98bf46639de368d6f8093a9548690539e4b9fa +size 2160 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6d.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6d.imageset/Contents.json new file mode 100644 index 0000000000..f223ec8d8f --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6d.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka-girl-6d.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6d.imageset/avatars_leka-girl-6d.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6d.imageset/avatars_leka-girl-6d.svg new file mode 100644 index 0000000000..dff3bc246e --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka-girl-6d.imageset/avatars_leka-girl-6d.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ee0a77a2698407fdd8b22e2dacc956a28727301c4d028e9e9d24614c8f26e2b +size 2160 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_astronaut.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_astronaut.imageset/Contents.json new file mode 100644 index 0000000000..c4a8c839f0 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_astronaut.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka_astronaut.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_astronaut.imageset/avatars_leka_astronaut.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_astronaut.imageset/avatars_leka_astronaut.svg new file mode 100644 index 0000000000..a404237811 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_astronaut.imageset/avatars_leka_astronaut.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bd059e462ade0833bfa02af8ff027f486c18ee4bec1c632874eb8ccf573c8a2 +size 5867 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_cloud.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_cloud.imageset/Contents.json new file mode 100644 index 0000000000..0d73d0c5ff --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_cloud.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka_cloud.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_cloud.imageset/avatars_leka_cloud.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_cloud.imageset/avatars_leka_cloud.svg new file mode 100644 index 0000000000..40fd632395 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_cloud.imageset/avatars_leka_cloud.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ee521518ec4755fea48effdede671931f3c0bbcdba33c6046519815ef261a09 +size 3753 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_cook.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_cook.imageset/Contents.json new file mode 100644 index 0000000000..1e8fd5bd00 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_cook.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka_cook.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_cook.imageset/avatars_leka_cook.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_cook.imageset/avatars_leka_cook.svg new file mode 100644 index 0000000000..9ffd700e05 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_cook.imageset/avatars_leka_cook.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc55005a5c482a8bd7e675f48abf77db50fd1ac92c12d1719b4d9b657fd4679c +size 4968 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_doctor.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_doctor.imageset/Contents.json new file mode 100644 index 0000000000..ba7583ff68 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_doctor.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka_doctor.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_doctor.imageset/avatars_leka_doctor.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_doctor.imageset/avatars_leka_doctor.svg new file mode 100644 index 0000000000..6a45394847 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_doctor.imageset/avatars_leka_doctor.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b8d3b82d8332748234e7b76b267cb7aaaf88fc504c088f0af7d05af037a6065 +size 3903 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_explorer.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_explorer.imageset/Contents.json new file mode 100644 index 0000000000..2770b12a07 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_explorer.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka_explorer.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_explorer.imageset/avatars_leka_explorer.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_explorer.imageset/avatars_leka_explorer.svg new file mode 100644 index 0000000000..a9de386948 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_explorer.imageset/avatars_leka_explorer.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb4aa0717370126ecb5a1693893ab24b8a1d93932297b27f62373654131a2a31 +size 2450 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_flake.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_flake.imageset/Contents.json new file mode 100644 index 0000000000..83da013131 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_flake.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka_flake.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_flake.imageset/avatars_leka_flake.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_flake.imageset/avatars_leka_flake.svg new file mode 100644 index 0000000000..23d96decff --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_flake.imageset/avatars_leka_flake.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76f40fbcd5e7725be24cc556dc8405e86a383d28d5f17cdb48dd89bdb9b155ac +size 6773 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_marine.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_marine.imageset/Contents.json new file mode 100644 index 0000000000..5f18eb709f --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_marine.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka_marine.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_marine.imageset/avatars_leka_marine.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_marine.imageset/avatars_leka_marine.svg new file mode 100644 index 0000000000..1f3dcab828 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_marine.imageset/avatars_leka_marine.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8748205a03fee0f4cff2a45d4978dad588a743a3d9d33015cddcda3c0e66ee6e +size 7863 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_moon.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_moon.imageset/Contents.json new file mode 100644 index 0000000000..b7b851ebec --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_moon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka_moon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_moon.imageset/avatars_leka_moon.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_moon.imageset/avatars_leka_moon.svg new file mode 100644 index 0000000000..6678ff3a0d --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_moon.imageset/avatars_leka_moon.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ccac3347ba319e1b6b1701561e4e62adde041d6fbb7c50fd5c8edae4167cd64d +size 7897 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_pirate.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_pirate.imageset/Contents.json new file mode 100644 index 0000000000..f889763b7f --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_pirate.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka_pirate.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_pirate.imageset/avatars_leka_pirate.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_pirate.imageset/avatars_leka_pirate.svg new file mode 100644 index 0000000000..be5c7480eb --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_pirate.imageset/avatars_leka_pirate.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a9517a866bd02b6802f09525b654a79453a9a3f89c423a3420b765ca9d47bde +size 7770 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_star.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_star.imageset/Contents.json new file mode 100644 index 0000000000..145bfb1e10 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_star.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka_star.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_star.imageset/avatars_leka_star.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_star.imageset/avatars_leka_star.svg new file mode 100644 index 0000000000..bf923c5e90 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_star.imageset/avatars_leka_star.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6973e7ad142ee8228b7a9c39b81f148e0b3df63187f534964ac503cf22191b80 +size 3793 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_blue.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_blue.imageset/Contents.json new file mode 100644 index 0000000000..2c460318fd --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_blue.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka_sunglasses_blue.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_blue.imageset/avatars_leka_sunglasses_blue.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_blue.imageset/avatars_leka_sunglasses_blue.svg new file mode 100644 index 0000000000..8324f15d84 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_blue.imageset/avatars_leka_sunglasses_blue.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdc41783525c06c451939302d221db0bc1740e5d44bfa71c19b2a94db060df05 +size 227476 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_green.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_green.imageset/Contents.json new file mode 100644 index 0000000000..5cb93455b7 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_green.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka_sunglasses_green.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_green.imageset/avatars_leka_sunglasses_green.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_green.imageset/avatars_leka_sunglasses_green.svg new file mode 100644 index 0000000000..e25d0f3863 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_green.imageset/avatars_leka_sunglasses_green.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4a58a69dd650110a01b0e7ab6803aa3966971e2297fc0d6e193d3b4515dd565 +size 227476 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_pink.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_pink.imageset/Contents.json new file mode 100644 index 0000000000..ad4b44708a --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_pink.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka_sunglasses_pink.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_pink.imageset/avatars_leka_sunglasses_pink.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_pink.imageset/avatars_leka_sunglasses_pink.svg new file mode 100644 index 0000000000..8852186a95 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_pink.imageset/avatars_leka_sunglasses_pink.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82133152814c29fde1c58c593a6799573af3d9392035d800539ba9050c1e620f +size 227493 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_yellow.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_yellow.imageset/Contents.json new file mode 100644 index 0000000000..34d972c805 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_yellow.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_leka_sunglasses_yellow.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_yellow.imageset/avatars_leka_sunglasses_yellow.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_yellow.imageset/avatars_leka_sunglasses_yellow.svg new file mode 100644 index 0000000000..eb81ceded3 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_leka_sunglasses_yellow.imageset/avatars_leka_sunglasses_yellow.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67ec35b8dc81f67b8af8d4b3923a637d1f686c9ee11437e22e87dbd5aad203fe +size 227566 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-bird_yellow-0071.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-bird_yellow-0071.imageset/Contents.json new file mode 100644 index 0000000000..6c87483955 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-bird_yellow-0071.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-animals-farm-bird_yellow-0071.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-bird_yellow-0071.imageset/avatars_pictograms-animals-farm-bird_yellow-0071.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-bird_yellow-0071.imageset/avatars_pictograms-animals-farm-bird_yellow-0071.svg new file mode 100644 index 0000000000..f139405099 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-bird_yellow-0071.imageset/avatars_pictograms-animals-farm-bird_yellow-0071.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:444e52c4ed5a2aaa3460b6d1eca3d5ae0c41f40505529004590fafeed33ae06d +size 1485 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-horse_brown-006A.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-horse_brown-006A.imageset/Contents.json new file mode 100644 index 0000000000..fcf4e03833 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-horse_brown-006A.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-animals-farm-horse_brown-006A.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-horse_brown-006A.imageset/avatars_pictograms-animals-farm-horse_brown-006A.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-horse_brown-006A.imageset/avatars_pictograms-animals-farm-horse_brown-006A.svg new file mode 100644 index 0000000000..058eaba69a --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-horse_brown-006A.imageset/avatars_pictograms-animals-farm-horse_brown-006A.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b4c51464a846a4ae9b8f82a890580247838602de1533f2709b60868a0b4ef25 +size 2453 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-rooster_white-006B.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-rooster_white-006B.imageset/Contents.json new file mode 100644 index 0000000000..743ef4e534 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-rooster_white-006B.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-animals-farm-rooster_white-006B.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-rooster_white-006B.imageset/avatars_pictograms-animals-farm-rooster_white-006B.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-rooster_white-006B.imageset/avatars_pictograms-animals-farm-rooster_white-006B.svg new file mode 100644 index 0000000000..bbc4b18eef --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-farm-rooster_white-006B.imageset/avatars_pictograms-animals-farm-rooster_white-006B.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f45963e62f5dff6da8a5beb98f4b880a2a97b2c02fc8439155e032cbf27ef08f +size 2799 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-bear_brown-005E.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-bear_brown-005E.imageset/Contents.json new file mode 100644 index 0000000000..27e366bce8 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-bear_brown-005E.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-animals-forest-bear_brown-005E.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-bear_brown-005E.imageset/avatars_pictograms-animals-forest-bear_brown-005E.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-bear_brown-005E.imageset/avatars_pictograms-animals-forest-bear_brown-005E.svg new file mode 100644 index 0000000000..d80db369cd --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-bear_brown-005E.imageset/avatars_pictograms-animals-forest-bear_brown-005E.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:054db9911278c9ed2ea98a44e5ea8fdf1e4eedf7659f7416c7df974d5988ec5c +size 2429 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-fox_orange-0064.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-fox_orange-0064.imageset/Contents.json new file mode 100644 index 0000000000..0a5ef22779 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-fox_orange-0064.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-animals-forest-fox_orange-0064.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-fox_orange-0064.imageset/avatars_pictograms-animals-forest-fox_orange-0064.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-fox_orange-0064.imageset/avatars_pictograms-animals-forest-fox_orange-0064.svg new file mode 100644 index 0000000000..dd856a97e1 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-fox_orange-0064.imageset/avatars_pictograms-animals-forest-fox_orange-0064.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:037e0f0645fb00752e37daf3063594af05e92fbb946d5fe128a0b863d9602bb0 +size 2046 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-hedgehog_brown-0062.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-hedgehog_brown-0062.imageset/Contents.json new file mode 100644 index 0000000000..c24c1f31a8 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-hedgehog_brown-0062.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-animals-forest-hedgehog_brown-0062.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-hedgehog_brown-0062.imageset/avatars_pictograms-animals-forest-hedgehog_brown-0062.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-hedgehog_brown-0062.imageset/avatars_pictograms-animals-forest-hedgehog_brown-0062.svg new file mode 100644 index 0000000000..a18b12288b --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-hedgehog_brown-0062.imageset/avatars_pictograms-animals-forest-hedgehog_brown-0062.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7cc067f4f43262d0fd0451a4c357faf1e50de2e8522f21ca2cbab1949a2478d +size 4345 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-rabbit_gray-0061.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-rabbit_gray-0061.imageset/Contents.json new file mode 100644 index 0000000000..2ee9d813d6 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-rabbit_gray-0061.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-animals-forest-rabbit_gray-0061.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-rabbit_gray-0061.imageset/avatars_pictograms-animals-forest-rabbit_gray-0061.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-rabbit_gray-0061.imageset/avatars_pictograms-animals-forest-rabbit_gray-0061.svg new file mode 100644 index 0000000000..4897d36249 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-rabbit_gray-0061.imageset/avatars_pictograms-animals-forest-rabbit_gray-0061.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9cad1c8986302a7f4ab1950012193fe96ee49900315f987db00af286d9416da +size 1688 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-squirrel_orange-005C.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-squirrel_orange-005C.imageset/Contents.json new file mode 100644 index 0000000000..9c4c653ab4 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-squirrel_orange-005C.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-animals-forest-squirrel_orange-005C.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-squirrel_orange-005C.imageset/avatars_pictograms-animals-forest-squirrel_orange-005C.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-squirrel_orange-005C.imageset/avatars_pictograms-animals-forest-squirrel_orange-005C.svg new file mode 100644 index 0000000000..f8b9ef3fce --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-forest-squirrel_orange-005C.imageset/avatars_pictograms-animals-forest-squirrel_orange-005C.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cdfecd8a6198a47ce862e54fa332e2e8a8bc1cadb94e692f1c4c7221e54f649 +size 2486 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-pets-turtle_green-0056.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-pets-turtle_green-0056.imageset/Contents.json new file mode 100644 index 0000000000..6b31561843 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-pets-turtle_green-0056.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-animals-pets-turtle_green-0056.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-pets-turtle_green-0056.imageset/avatars_pictograms-animals-pets-turtle_green-0056.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-pets-turtle_green-0056.imageset/avatars_pictograms-animals-pets-turtle_green-0056.svg new file mode 100644 index 0000000000..a6dad08cba --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-pets-turtle_green-0056.imageset/avatars_pictograms-animals-pets-turtle_green-0056.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f3e932355df3019cd9fa48d446e46db5bd41c04d0ff2d4714f8ccbfd08ec0f8 +size 1971 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-pets_fish_blue-0055.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-pets_fish_blue-0055.imageset/Contents.json new file mode 100644 index 0000000000..b1d12249ab --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-pets_fish_blue-0055.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-animals-pets_fish_blue-0055.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-pets_fish_blue-0055.imageset/avatars_pictograms-animals-pets_fish_blue-0055.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-pets_fish_blue-0055.imageset/avatars_pictograms-animals-pets_fish_blue-0055.svg new file mode 100644 index 0000000000..0039d56c9f --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-pets_fish_blue-0055.imageset/avatars_pictograms-animals-pets_fish_blue-0055.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5fdbc4266092af30fb37543f641bec8756543d3068f8e5fa154ebbd924648a16 +size 2395 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-elephant_gray-0085.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-elephant_gray-0085.imageset/Contents.json new file mode 100644 index 0000000000..5ad83ebdcf --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-elephant_gray-0085.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-animals-savanna-elephant_gray-0085.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-elephant_gray-0085.imageset/avatars_pictograms-animals-savanna-elephant_gray-0085.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-elephant_gray-0085.imageset/avatars_pictograms-animals-savanna-elephant_gray-0085.svg new file mode 100644 index 0000000000..733568ae3f --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-elephant_gray-0085.imageset/avatars_pictograms-animals-savanna-elephant_gray-0085.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ac6b993e848677fe8438ee7cd3ebafa183477730a3dd8f124b56673d946ef1e +size 2976 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-giraffe_yellow-0081.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-giraffe_yellow-0081.imageset/Contents.json new file mode 100644 index 0000000000..ba4950add0 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-giraffe_yellow-0081.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-animals-savanna-giraffe_yellow-0081.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-giraffe_yellow-0081.imageset/avatars_pictograms-animals-savanna-giraffe_yellow-0081.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-giraffe_yellow-0081.imageset/avatars_pictograms-animals-savanna-giraffe_yellow-0081.svg new file mode 100644 index 0000000000..ce9cb54546 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-giraffe_yellow-0081.imageset/avatars_pictograms-animals-savanna-giraffe_yellow-0081.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d032e339743bd0e2a090e07db4aa96e6f466ccb114d7c9448f318fa9d8027680 +size 4767 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-kangaroo_brown-0078.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-kangaroo_brown-0078.imageset/Contents.json new file mode 100644 index 0000000000..6147156654 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-kangaroo_brown-0078.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-animals-savanna-kangaroo_brown-0078.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-kangaroo_brown-0078.imageset/avatars_pictograms-animals-savanna-kangaroo_brown-0078.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-kangaroo_brown-0078.imageset/avatars_pictograms-animals-savanna-kangaroo_brown-0078.svg new file mode 100644 index 0000000000..5a6e952745 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-kangaroo_brown-0078.imageset/avatars_pictograms-animals-savanna-kangaroo_brown-0078.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b6613fd62c93270adc44ed78a4e7ee3f48bf28d5ff0da1870138a75068d7b07 +size 3661 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-koala_gray-0077.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-koala_gray-0077.imageset/Contents.json new file mode 100644 index 0000000000..a0ee7f6280 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-koala_gray-0077.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-animals-savanna-koala_gray-0077.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-koala_gray-0077.imageset/avatars_pictograms-animals-savanna-koala_gray-0077.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-koala_gray-0077.imageset/avatars_pictograms-animals-savanna-koala_gray-0077.svg new file mode 100644 index 0000000000..78f28e846f --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-koala_gray-0077.imageset/avatars_pictograms-animals-savanna-koala_gray-0077.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea38e206b60e3472b104d3163f94ba35116a3bbab971cf8acba2d2552707e6c0 +size 2817 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-lion_brown-0082.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-lion_brown-0082.imageset/Contents.json new file mode 100644 index 0000000000..4e1e888ca0 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-lion_brown-0082.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-animals-savanna-lion_brown-0082.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-lion_brown-0082.imageset/avatars_pictograms-animals-savanna-lion_brown-0082.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-lion_brown-0082.imageset/avatars_pictograms-animals-savanna-lion_brown-0082.svg new file mode 100644 index 0000000000..0f09ef8a61 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-savanna-lion_brown-0082.imageset/avatars_pictograms-animals-savanna-lion_brown-0082.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86d31fc2231889b80719d0bcbecee397db86cae8a7e3c54f193c80f0c5f0b8f7 +size 3723 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-sea-crab_red-003E.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-sea-crab_red-003E.imageset/Contents.json new file mode 100644 index 0000000000..a3e6e270eb --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-sea-crab_red-003E.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-animals-sea-crab_red-003E.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-sea-crab_red-003E.imageset/avatars_pictograms-animals-sea-crab_red-003E.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-sea-crab_red-003E.imageset/avatars_pictograms-animals-sea-crab_red-003E.svg new file mode 100644 index 0000000000..56a451e22a --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-sea-crab_red-003E.imageset/avatars_pictograms-animals-sea-crab_red-003E.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:331b310938e30a102b1184106741487d61645620553d3a72c989620ece38947c +size 3273 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-sea-turtle_green-0041.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-sea-turtle_green-0041.imageset/Contents.json new file mode 100644 index 0000000000..76045c7c65 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-sea-turtle_green-0041.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-animals-sea-turtle_green-0041.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-sea-turtle_green-0041.imageset/avatars_pictograms-animals-sea-turtle_green-0041.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-sea-turtle_green-0041.imageset/avatars_pictograms-animals-sea-turtle_green-0041.svg new file mode 100644 index 0000000000..b62e5dfb5b --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-animals-sea-turtle_green-0041.imageset/avatars_pictograms-animals-sea-turtle_green-0041.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9f6a0a87c91668ca3bd0dd0469f79d40af70ceb6b3ff3268f613dd1e2c8b9be +size 1971 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-apple_green-0100.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-apple_green-0100.imageset/Contents.json new file mode 100644 index 0000000000..f1a942a40b --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-apple_green-0100.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-fruits-apple_green-0100.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-apple_green-0100.imageset/avatars_pictograms-foods-fruits-apple_green-0100.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-apple_green-0100.imageset/avatars_pictograms-foods-fruits-apple_green-0100.svg new file mode 100644 index 0000000000..ae7a670ae5 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-apple_green-0100.imageset/avatars_pictograms-foods-fruits-apple_green-0100.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:999555d8e7da04a7696dd4001effe20fabd6bb233ba15eb4c63fa30fe3a16315 +size 943 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-apple_red-0101.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-apple_red-0101.imageset/Contents.json new file mode 100644 index 0000000000..4ee89796b5 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-apple_red-0101.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-fruits-apple_red-0101.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-apple_red-0101.imageset/avatars_pictograms-foods-fruits-apple_red-0101.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-apple_red-0101.imageset/avatars_pictograms-foods-fruits-apple_red-0101.svg new file mode 100644 index 0000000000..827378124b --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-apple_red-0101.imageset/avatars_pictograms-foods-fruits-apple_red-0101.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:745e571d67d8d55ec0c5ff052f53e4bb658e0821bac117daa50c367f95b7aace +size 1026 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-banana_yellow-00FB.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-banana_yellow-00FB.imageset/Contents.json new file mode 100644 index 0000000000..24929b9134 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-banana_yellow-00FB.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-fruits-banana_yellow-00FB.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-banana_yellow-00FB.imageset/avatars_pictograms-foods-fruits-banana_yellow-00FB.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-banana_yellow-00FB.imageset/avatars_pictograms-foods-fruits-banana_yellow-00FB.svg new file mode 100644 index 0000000000..71841f7034 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-banana_yellow-00FB.imageset/avatars_pictograms-foods-fruits-banana_yellow-00FB.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d79ad5978c35da4e11fe851afe1c94d16e83ef319b33f027bbaf6a63421adcf +size 1144 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-cherry_red-00FF.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-cherry_red-00FF.imageset/Contents.json new file mode 100644 index 0000000000..9de661fa15 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-cherry_red-00FF.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-fruits-cherry_red-00FF.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-cherry_red-00FF.imageset/avatars_pictograms-foods-fruits-cherry_red-00FF.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-cherry_red-00FF.imageset/avatars_pictograms-foods-fruits-cherry_red-00FF.svg new file mode 100644 index 0000000000..f5958d69b9 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-cherry_red-00FF.imageset/avatars_pictograms-foods-fruits-cherry_red-00FF.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebdee22563f6b45a64cdd8454fab599d86a2d43e7ff2975a6e331016da2aaa2d +size 1493 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-grape_purple-00FE.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-grape_purple-00FE.imageset/Contents.json new file mode 100644 index 0000000000..68552a6e3e --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-grape_purple-00FE.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-fruits-grape_purple-00FE.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-grape_purple-00FE.imageset/avatars_pictograms-foods-fruits-grape_purple-00FE.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-grape_purple-00FE.imageset/avatars_pictograms-foods-fruits-grape_purple-00FE.svg new file mode 100644 index 0000000000..1f11b93fec --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-grape_purple-00FE.imageset/avatars_pictograms-foods-fruits-grape_purple-00FE.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54433b739a0bd7d4105e247e8809e71741f1d4a6994316250de5641d42df8646 +size 1963 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-kiwi_green-00F8.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-kiwi_green-00F8.imageset/Contents.json new file mode 100644 index 0000000000..aa687d225d --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-kiwi_green-00F8.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-fruits-kiwi_green-00F8.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-kiwi_green-00F8.imageset/avatars_pictograms-foods-fruits-kiwi_green-00F8.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-kiwi_green-00F8.imageset/avatars_pictograms-foods-fruits-kiwi_green-00F8.svg new file mode 100644 index 0000000000..f3183869dd --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-kiwi_green-00F8.imageset/avatars_pictograms-foods-fruits-kiwi_green-00F8.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:340f706ba978b38cef716e28277389193b23441d6fcd6a3a63efd0debcd0442b +size 6517 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-lemon_yellow-00F7.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-lemon_yellow-00F7.imageset/Contents.json new file mode 100644 index 0000000000..8de054db25 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-lemon_yellow-00F7.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-fruits-lemon_yellow-00F7.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-lemon_yellow-00F7.imageset/avatars_pictograms-foods-fruits-lemon_yellow-00F7.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-lemon_yellow-00F7.imageset/avatars_pictograms-foods-fruits-lemon_yellow-00F7.svg new file mode 100644 index 0000000000..82a98c1761 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-lemon_yellow-00F7.imageset/avatars_pictograms-foods-fruits-lemon_yellow-00F7.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddd0400cd6f5d21220e58acbd1166a9a6ad852f4a122294d2a2580d4d01dba31 +size 886 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-pear_yellow-00FC.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-pear_yellow-00FC.imageset/Contents.json new file mode 100644 index 0000000000..c16e761486 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-pear_yellow-00FC.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-fruits-pear_yellow-00FC.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-pear_yellow-00FC.imageset/avatars_pictograms-foods-fruits-pear_yellow-00FC.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-pear_yellow-00FC.imageset/avatars_pictograms-foods-fruits-pear_yellow-00FC.svg new file mode 100644 index 0000000000..ec9675f41b --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-pear_yellow-00FC.imageset/avatars_pictograms-foods-fruits-pear_yellow-00FC.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e55a8c0323e2c152192cf938de79391061527f443a6c1a38e0953884520662c +size 917 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-pineapple_orange-00F9.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-pineapple_orange-00F9.imageset/Contents.json new file mode 100644 index 0000000000..8911a1a703 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-pineapple_orange-00F9.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-fruits-pineapple_orange-00F9.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-pineapple_orange-00F9.imageset/avatars_pictograms-foods-fruits-pineapple_orange-00F9.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-pineapple_orange-00F9.imageset/avatars_pictograms-foods-fruits-pineapple_orange-00F9.svg new file mode 100644 index 0000000000..7093635f04 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-pineapple_orange-00F9.imageset/avatars_pictograms-foods-fruits-pineapple_orange-00F9.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac326cb2854cc9bb86beae0723a5ba32beb208f174d04f8daa0a4c52611bf42e +size 3840 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-strawberry_red-00FD.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-strawberry_red-00FD.imageset/Contents.json new file mode 100644 index 0000000000..f9d3ed5d9f --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-strawberry_red-00FD.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-fruits-strawberry_red-00FD.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-strawberry_red-00FD.imageset/avatars_pictograms-foods-fruits-strawberry_red-00FD.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-strawberry_red-00FD.imageset/avatars_pictograms-foods-fruits-strawberry_red-00FD.svg new file mode 100644 index 0000000000..e24f60ece9 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-strawberry_red-00FD.imageset/avatars_pictograms-foods-fruits-strawberry_red-00FD.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe69bc4df174e9b2b83b508d584dbc7d3fd81346c8cd8fddca9d66651bc18e3d +size 1825 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-watermelon_red-00FA.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-watermelon_red-00FA.imageset/Contents.json new file mode 100644 index 0000000000..5f0a4a21f7 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-watermelon_red-00FA.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-fruits-watermelon_red-00FA.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-watermelon_red-00FA.imageset/avatars_pictograms-foods-fruits-watermelon_red-00FA.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-watermelon_red-00FA.imageset/avatars_pictograms-foods-fruits-watermelon_red-00FA.svg new file mode 100644 index 0000000000..e3afe0423c --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-fruits-watermelon_red-00FA.imageset/avatars_pictograms-foods-fruits-watermelon_red-00FA.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1e7ce7772e8c7ae201ae5a9e5e0bc138cea063417245b90bfbd6ebd5a169b77 +size 2326 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-avocado_green-00E1.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-avocado_green-00E1.imageset/Contents.json new file mode 100644 index 0000000000..ab7661ef4b --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-avocado_green-00E1.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-vegetables-avocado_green-00E1.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-avocado_green-00E1.imageset/avatars_pictograms-foods-vegetables-avocado_green-00E1.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-avocado_green-00E1.imageset/avatars_pictograms-foods-vegetables-avocado_green-00E1.svg new file mode 100644 index 0000000000..22e0d2273a --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-avocado_green-00E1.imageset/avatars_pictograms-foods-vegetables-avocado_green-00E1.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15fa35f03ad3c2e0b6979cdba3871e55edfcf4b4e9d2b930c37478e555950aa3 +size 1080 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-broccoli_green-00E5.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-broccoli_green-00E5.imageset/Contents.json new file mode 100644 index 0000000000..cf5d5da0af --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-broccoli_green-00E5.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-vegetables-broccoli_green-00E5.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-broccoli_green-00E5.imageset/avatars_pictograms-foods-vegetables-broccoli_green-00E5.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-broccoli_green-00E5.imageset/avatars_pictograms-foods-vegetables-broccoli_green-00E5.svg new file mode 100644 index 0000000000..5396274539 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-broccoli_green-00E5.imageset/avatars_pictograms-foods-vegetables-broccoli_green-00E5.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7da78eedef9f1e2c8d574ec1aaf9df61484b8385cf39c8ed4ec1bc53429ea8b8 +size 1656 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-carrot_orange-00E6.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-carrot_orange-00E6.imageset/Contents.json new file mode 100644 index 0000000000..7be1c5bca6 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-carrot_orange-00E6.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-vegetables-carrot_orange-00E6.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-carrot_orange-00E6.imageset/avatars_pictograms-foods-vegetables-carrot_orange-00E6.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-carrot_orange-00E6.imageset/avatars_pictograms-foods-vegetables-carrot_orange-00E6.svg new file mode 100644 index 0000000000..29a8aca351 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-carrot_orange-00E6.imageset/avatars_pictograms-foods-vegetables-carrot_orange-00E6.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47873135194ff90145c55f4f9f2247d098d360b81f08900a759fc758f879a300 +size 1960 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-corn_yellow-00E3.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-corn_yellow-00E3.imageset/Contents.json new file mode 100644 index 0000000000..1f95571553 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-corn_yellow-00E3.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-vegetables-corn_yellow-00E3.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-corn_yellow-00E3.imageset/avatars_pictograms-foods-vegetables-corn_yellow-00E3.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-corn_yellow-00E3.imageset/avatars_pictograms-foods-vegetables-corn_yellow-00E3.svg new file mode 100644 index 0000000000..1007473a6a --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-corn_yellow-00E3.imageset/avatars_pictograms-foods-vegetables-corn_yellow-00E3.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4f6e0c8074096aca78d2c57fe7a8346730871a618190578d83e11f3d08acca7 +size 2561 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-eggplant_purple-00E4.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-eggplant_purple-00E4.imageset/Contents.json new file mode 100644 index 0000000000..ecc94dba1f --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-eggplant_purple-00E4.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-vegetables-eggplant_purple-00E4.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-eggplant_purple-00E4.imageset/avatars_pictograms-foods-vegetables-eggplant_purple-00E4.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-eggplant_purple-00E4.imageset/avatars_pictograms-foods-vegetables-eggplant_purple-00E4.svg new file mode 100644 index 0000000000..9dbf978cef --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-eggplant_purple-00E4.imageset/avatars_pictograms-foods-vegetables-eggplant_purple-00E4.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7cb2338226e6499f1656a14dd8341bd5deb2ca850835469ae12cb290a954f59 +size 1323 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-onion_yellow-00E8.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-onion_yellow-00E8.imageset/Contents.json new file mode 100644 index 0000000000..be65ce0b17 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-onion_yellow-00E8.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-vegetables-onion_yellow-00E8.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-onion_yellow-00E8.imageset/avatars_pictograms-foods-vegetables-onion_yellow-00E8.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-onion_yellow-00E8.imageset/avatars_pictograms-foods-vegetables-onion_yellow-00E8.svg new file mode 100644 index 0000000000..57f74fc34e --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-onion_yellow-00E8.imageset/avatars_pictograms-foods-vegetables-onion_yellow-00E8.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c92671b1dd2b4fe4aa33a7c5518b801acdb4637b252d3ebf8c9bb13efc385c4 +size 1392 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-potato_yellow_1-00E9.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-potato_yellow_1-00E9.imageset/Contents.json new file mode 100644 index 0000000000..9a7414d822 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-potato_yellow_1-00E9.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-vegetables-potato_yellow_1-00E9.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-potato_yellow_1-00E9.imageset/avatars_pictograms-foods-vegetables-potato_yellow_1-00E9.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-potato_yellow_1-00E9.imageset/avatars_pictograms-foods-vegetables-potato_yellow_1-00E9.svg new file mode 100644 index 0000000000..12d9d7b9aa --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-potato_yellow_1-00E9.imageset/avatars_pictograms-foods-vegetables-potato_yellow_1-00E9.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e8a2d0e709556585668962c279279f3c0858226a203a47ffdf4ee74d31e8e4a +size 1110 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-salad_green_1-00EA.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-salad_green_1-00EA.imageset/Contents.json new file mode 100644 index 0000000000..84a83a7c12 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-salad_green_1-00EA.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-vegetables-salad_green_1-00EA.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-salad_green_1-00EA.imageset/avatars_pictograms-foods-vegetables-salad_green_1-00EA.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-salad_green_1-00EA.imageset/avatars_pictograms-foods-vegetables-salad_green_1-00EA.svg new file mode 100644 index 0000000000..dd8e81cbdc --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-salad_green_1-00EA.imageset/avatars_pictograms-foods-vegetables-salad_green_1-00EA.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:957da60fe703bd73b4eb311378bb88227beeef0f2d1600b7e9ddf431a4b1f2b9 +size 1924 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-tomato_red-00E2.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-tomato_red-00E2.imageset/Contents.json new file mode 100644 index 0000000000..4ac9ccde58 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-tomato_red-00E2.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_pictograms-foods-vegetables-tomato_red-00E2.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-tomato_red-00E2.imageset/avatars_pictograms-foods-vegetables-tomato_red-00E2.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-tomato_red-00E2.imageset/avatars_pictograms-foods-vegetables-tomato_red-00E2.svg new file mode 100644 index 0000000000..eb417371ef --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_pictograms-foods-vegetables-tomato_red-00E2.imageset/avatars_pictograms-foods-vegetables-tomato_red-00E2.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0eb67bd784020e3d5c59cf469f6b116d143e8c37eca71c26b04b8e50bed1a702 +size 783 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_sun.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_sun.imageset/Contents.json new file mode 100644 index 0000000000..95f4b6e48c --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_sun.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "avatars_sun.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/avatars_sun.imageset/avatars_sun.svg b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_sun.imageset/avatars_sun.svg new file mode 100644 index 0000000000..5901d2fbbf --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/avatars_sun.imageset/avatars_sun.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97d1f72bc94cfa736b2b7d22f7c017fc8ea50832922216fae8df56d8b4e9c4af +size 230285 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/accompanying_blue.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/accompanying_blue.imageset/Contents.json new file mode 100644 index 0000000000..c6cbd888c3 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/accompanying_blue.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "accompanyingBlue.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/accompanying_blue.imageset/accompanyingBlue.svg b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/accompanying_blue.imageset/accompanyingBlue.svg new file mode 100644 index 0000000000..36d89070c0 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/accompanying_blue.imageset/accompanyingBlue.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acb60895400d75602a8616ddd9f890b7f4fe1e0e84a4ad687e99cdb902eb1d54 +size 5650 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/accompanying_white.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/accompanying_white.imageset/Contents.json new file mode 100644 index 0000000000..e4d2802c63 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/accompanying_white.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "accompanying.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/accompanying_white.imageset/accompanying.svg b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/accompanying_white.imageset/accompanying.svg new file mode 100644 index 0000000000..ecd4c0caff --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/accompanying_white.imageset/accompanying.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63a8094fdbedb92328e592491728961b975dfdd64c8f25fd78bb58aa2efd57dd +size 5768 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/question_mark.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/question_mark.imageset/Contents.json new file mode 100644 index 0000000000..5657f1f909 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/question_mark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "question_mark.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/question_mark.imageset/question_mark.svg b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/question_mark.imageset/question_mark.svg new file mode 100644 index 0000000000..9f066530f0 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/question_mark.imageset/question_mark.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:731668ac65311c6cd42946707f9d86dee250c67e037f23a85a9b7f0af3a53c49 +size 2453 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/question_mark_blue.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/question_mark_blue.imageset/Contents.json new file mode 100644 index 0000000000..70b8e22015 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/question_mark_blue.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "question_mark_blue.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/question_mark_blue.imageset/question_mark_blue.svg b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/question_mark_blue.imageset/question_mark_blue.svg new file mode 100644 index 0000000000..5594a17c30 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/question_mark_blue.imageset/question_mark_blue.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bd92bbce7eb643ad855557e0eb3bc3fb1d6a04044edd4bd9062c5d47c5f4438 +size 2551 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/user_blue.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/user_blue.imageset/Contents.json new file mode 100644 index 0000000000..08d76aab6b --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/user_blue.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "userBlue.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/user_blue.imageset/userBlue.svg b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/user_blue.imageset/userBlue.svg new file mode 100644 index 0000000000..ac36f4b764 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/user_blue.imageset/userBlue.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0ff73dc498b26daedc8f859d700edde0f4f702946bfadbda049721a6c5ac3bd +size 2836 diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/user_white.imageset/Contents.json b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/user_white.imageset/Contents.json new file mode 100644 index 0000000000..abd9b2f326 --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/user_white.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "user.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/user_white.imageset/user.svg b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/user_white.imageset/user.svg new file mode 100644 index 0000000000..eaff4bdfdb --- /dev/null +++ b/Modules/DesignKit/Resources/Avatars.xcassets/placeholders/user_white.imageset/user.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd304409e744e53ce650560082392d87dd008eb492efb0ab50b3adc0e81a2abd +size 2954 diff --git a/Modules/DesignKit/Resources/Colors.xcassets/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Colors.xcassets/blueGray.colorset/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/blueGray.colorset/Contents.json new file mode 100644 index 0000000000..d7d840844b --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/blueGray.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.831", + "green" : "0.733", + "red" : "0.639" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.831", + "green" : "0.733", + "red" : "0.639" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Colors.xcassets/bravoHighlights.colorset/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/bravoHighlights.colorset/Contents.json new file mode 100644 index 0000000000..1f6b0a72b3 --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/bravoHighlights.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.980", + "green" : "0.788", + "red" : "0.475" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Colors.xcassets/btnDarkBlue.colorset/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/btnDarkBlue.colorset/Contents.json new file mode 100644 index 0000000000..13c183f5fe --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/btnDarkBlue.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.475", + "green" : "0.137", + "red" : "0.110" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.475", + "green" : "0.137", + "red" : "0.110" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Colors.xcassets/btnLightBlue.colorset/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/btnLightBlue.colorset/Contents.json new file mode 100644 index 0000000000..f9edd244b8 --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/btnLightBlue.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.914", + "green" : "0.824", + "red" : "0.671" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.914", + "green" : "0.824", + "red" : "0.671" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Colors.xcassets/chevron.colorset/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/chevron.colorset/Contents.json new file mode 100644 index 0000000000..202eb8dfe2 --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/chevron.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.773", + "green" : "0.769", + "red" : "0.769" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.773", + "green" : "0.769", + "red" : "0.769" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Colors.xcassets/darkGray.colorset/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/darkGray.colorset/Contents.json new file mode 100644 index 0000000000..55bc8401a7 --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/darkGray.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.290", + "green" : "0.290", + "red" : "0.290" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.290", + "green" : "0.290", + "red" : "0.290" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Colors.xcassets/gameButtonBorder.colorset/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/gameButtonBorder.colorset/Contents.json new file mode 100644 index 0000000000..9bfd8e13ed --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/gameButtonBorder.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.965", + "green" : "0.965", + "red" : "0.965" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Colors.xcassets/lekaDarkBlue.colorset/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/lekaDarkBlue.colorset/Contents.json new file mode 100644 index 0000000000..2195a489b3 --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/lekaDarkBlue.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.608", + "green" : "0.341", + "red" : "0.039" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.608", + "green" : "0.341", + "red" : "0.039" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Colors.xcassets/lekaDarkGray.colorset/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/lekaDarkGray.colorset/Contents.json new file mode 100644 index 0000000000..21a27666b3 --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/lekaDarkGray.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.553", + "green" : "0.553", + "red" : "0.553" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.553", + "green" : "0.553", + "red" : "0.553" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Colors.xcassets/lekaGreen.colorset/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/lekaGreen.colorset/Contents.json new file mode 100644 index 0000000000..b3b99938fa --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/lekaGreen.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.212", + "green" : "0.808", + "red" : "0.686" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.212", + "green" : "0.808", + "red" : "0.686" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Colors.xcassets/lekaLightBlue.colorset/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/lekaLightBlue.colorset/Contents.json new file mode 100644 index 0000000000..d3863ef7a5 --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/lekaLightBlue.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.988", + "green" : "0.922", + "red" : "0.812" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.988", + "green" : "0.922", + "red" : "0.812" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Colors.xcassets/lekaLightGray.colorset/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/lekaLightGray.colorset/Contents.json new file mode 100644 index 0000000000..6606c1cfd5 --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/lekaLightGray.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.969", + "green" : "0.945", + "red" : "0.945" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.969", + "green" : "0.949", + "red" : "0.949" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Colors.xcassets/lekaOrange.colorset/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/lekaOrange.colorset/Contents.json new file mode 100644 index 0000000000..502d329d37 --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/lekaOrange.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.125", + "green" : "0.471", + "red" : "0.937" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.125", + "green" : "0.471", + "red" : "0.937" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Colors.xcassets/lekaSkyBlue.colorset/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/lekaSkyBlue.colorset/Contents.json new file mode 100644 index 0000000000..642e83a34a --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/lekaSkyBlue.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.835", + "green" : "0.682", + "red" : "0.341" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.835", + "green" : "0.682", + "red" : "0.341" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Colors.xcassets/lightGray.colorset/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/lightGray.colorset/Contents.json new file mode 100644 index 0000000000..04444bc8ff --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/lightGray.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.973", + "green" : "0.969", + "red" : "0.973" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.976", + "green" : "0.969", + "red" : "0.975" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Colors.xcassets/progressBar.colorset/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/progressBar.colorset/Contents.json new file mode 100644 index 0000000000..b6e0a4c247 --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/progressBar.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.847", + "green" : "0.843", + "red" : "0.843" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.847", + "green" : "0.843", + "red" : "0.843" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Colors.xcassets/xyloAttach.colorset/Contents.json b/Modules/DesignKit/Resources/Colors.xcassets/xyloAttach.colorset/Contents.json new file mode 100644 index 0000000000..92cffc9787 --- /dev/null +++ b/Modules/DesignKit/Resources/Colors.xcassets/xyloAttach.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0x8A", + "green" : "0xA6", + "red" : "0xDE" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Images.xcassets/Contents.json b/Modules/DesignKit/Resources/Images.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Images.xcassets/Tiles/Contents.json b/Modules/DesignKit/Resources/Images.xcassets/Tiles/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/Tiles/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Images.xcassets/Tiles/accompagnant_picto.imageset/Contents.json b/Modules/DesignKit/Resources/Images.xcassets/Tiles/accompagnant_picto.imageset/Contents.json new file mode 100644 index 0000000000..17e60cd8ad --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/Tiles/accompagnant_picto.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "accompagnant_picto.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Images.xcassets/Tiles/accompagnant_picto.imageset/accompagnant_picto.svg b/Modules/DesignKit/Resources/Images.xcassets/Tiles/accompagnant_picto.imageset/accompagnant_picto.svg new file mode 100644 index 0000000000..df8f13c919 --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/Tiles/accompagnant_picto.imageset/accompagnant_picto.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:247298b3c7070ece70b6496142a21b7dce772ecb13c857671a5df1b45fe00a02 +size 8389 diff --git a/Modules/DesignKit/Resources/Images.xcassets/Tiles/curriculums.imageset/Contents.json b/Modules/DesignKit/Resources/Images.xcassets/Tiles/curriculums.imageset/Contents.json new file mode 100644 index 0000000000..406a667a7f --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/Tiles/curriculums.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "curriculums.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Images.xcassets/Tiles/curriculums.imageset/curriculums.svg b/Modules/DesignKit/Resources/Images.xcassets/Tiles/curriculums.imageset/curriculums.svg new file mode 100644 index 0000000000..bcbfed282f --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/Tiles/curriculums.imageset/curriculums.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:095173d2e9ce7ff78f9abd93c1ff3f767b89ea4770bee2a3ef6d18d18805dd59 +size 3254 diff --git a/Modules/DesignKit/Resources/Images.xcassets/Tiles/magnifier.imageset/Contents.json b/Modules/DesignKit/Resources/Images.xcassets/Tiles/magnifier.imageset/Contents.json new file mode 100644 index 0000000000..d8771f0adc --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/Tiles/magnifier.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "magnifier.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Images.xcassets/Tiles/magnifier.imageset/magnifier.svg b/Modules/DesignKit/Resources/Images.xcassets/Tiles/magnifier.imageset/magnifier.svg new file mode 100644 index 0000000000..5f75c6a0fc --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/Tiles/magnifier.imageset/magnifier.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed9c730b53142e2f6ecdd21a3838b133bcc04ffbbb9ece7d909b7c57be6db49a +size 1583 diff --git a/Modules/DesignKit/Resources/Images.xcassets/Tiles/user.imageset/Contents.json b/Modules/DesignKit/Resources/Images.xcassets/Tiles/user.imageset/Contents.json new file mode 100644 index 0000000000..abd9b2f326 --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/Tiles/user.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "user.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Images.xcassets/Tiles/user.imageset/user.svg b/Modules/DesignKit/Resources/Images.xcassets/Tiles/user.imageset/user.svg new file mode 100644 index 0000000000..ba0a04c67a --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/Tiles/user.imageset/user.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:908f84a300fdba197d01888b087a32df748616fc68bc175c93f36c79d55a5195 +size 2647 diff --git a/Modules/DesignKit/Resources/Images.xcassets/Tiles/welcome.imageset/Contents.json b/Modules/DesignKit/Resources/Images.xcassets/Tiles/welcome.imageset/Contents.json new file mode 100644 index 0000000000..d7d196eff3 --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/Tiles/welcome.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "welcome.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Images.xcassets/Tiles/welcome.imageset/welcome.svg b/Modules/DesignKit/Resources/Images.xcassets/Tiles/welcome.imageset/welcome.svg new file mode 100644 index 0000000000..1554d7ea78 --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/Tiles/welcome.imageset/welcome.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:789679339ac64db6c002b34db09afb3de59993e59813f09b2a11794d2df923b0 +size 1030027 diff --git a/Modules/DesignKit/Resources/Images.xcassets/connection indicators/Contents.json b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Images.xcassets/connection indicators/cross.imageset/Contents.json b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/cross.imageset/Contents.json new file mode 100644 index 0000000000..061e093ba4 --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/cross.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "effective_1.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/Modules/DesignKit/Resources/Images.xcassets/connection indicators/cross.imageset/effective_1.svg b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/cross.imageset/effective_1.svg new file mode 100644 index 0000000000..3219e1bb7f --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/cross.imageset/effective_1.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64679a52379f1014d5ec5a67883f68eedaae35860248ad65a37552c4a903eb1c +size 1563 diff --git a/Modules/DesignKit/Resources/Images.xcassets/connection indicators/robot_connected.imageset/Contents.json b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/robot_connected.imageset/Contents.json new file mode 100644 index 0000000000..32f625d3f3 --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/robot_connected.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "robot_connected.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Images.xcassets/connection indicators/robot_connected.imageset/robot_connected.svg b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/robot_connected.imageset/robot_connected.svg new file mode 100644 index 0000000000..a54bb61152 --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/robot_connected.imageset/robot_connected.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b74b0ea336c4e59c2754d1c6632319f1fb57c396cc1be38ddc33070c52e4688 +size 907 diff --git a/Modules/DesignKit/Resources/Images.xcassets/connection indicators/robot_disconnected.imageset/Contents.json b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/robot_disconnected.imageset/Contents.json new file mode 100644 index 0000000000..42f9f13842 --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/robot_disconnected.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "robot_disconnected.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Images.xcassets/connection indicators/robot_disconnected.imageset/robot_disconnected.svg b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/robot_disconnected.imageset/robot_disconnected.svg new file mode 100644 index 0000000000..303f2afd6c --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/robot_disconnected.imageset/robot_disconnected.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb649092ab2a3724db05be097d609cd368ff157a36c09206a0bbdae2ef2bf444 +size 907 diff --git a/Modules/DesignKit/Resources/Images.xcassets/connection indicators/tick.imageset/Contents.json b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/tick.imageset/Contents.json new file mode 100644 index 0000000000..85738221dc --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/tick.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "effective_2.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Images.xcassets/connection indicators/tick.imageset/effective_2.svg b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/tick.imageset/effective_2.svg new file mode 100644 index 0000000000..eb7f304b8a --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/connection indicators/tick.imageset/effective_2.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59d3fb3d1618857a645827d808a795d4a87643491bea893a62f7d061c5cd9feb +size 1181 diff --git a/Modules/DesignKit/Resources/Images.xcassets/interface_cloud.imageset/Contents.json b/Modules/DesignKit/Resources/Images.xcassets/interface_cloud.imageset/Contents.json new file mode 100644 index 0000000000..e94584a215 --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/interface_cloud.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "interface_cloud.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Images.xcassets/interface_cloud.imageset/interface_cloud.svg b/Modules/DesignKit/Resources/Images.xcassets/interface_cloud.imageset/interface_cloud.svg new file mode 100644 index 0000000000..f198dba0d7 --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/interface_cloud.imageset/interface_cloud.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8036dd27bf51904fcd50cd2754384f697a16f6c13f570edbb356a200ad1024c6 +size 2265 diff --git a/Modules/DesignKit/Resources/Images.xcassets/person.talking.imageset/Contents.json b/Modules/DesignKit/Resources/Images.xcassets/person.talking.imageset/Contents.json new file mode 100644 index 0000000000..ff741ba4ce --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/person.talking.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "person.talking.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Images.xcassets/person.talking.imageset/person.talking.svg b/Modules/DesignKit/Resources/Images.xcassets/person.talking.imageset/person.talking.svg new file mode 100644 index 0000000000..f08d3e6144 --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/person.talking.imageset/person.talking.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ff09691ea11952699f5cd76a9a5abe8ae3ea91c1f60385356c78ab4fb23eb68 +size 2421 diff --git a/Modules/DesignKit/Resources/Images.xcassets/robot_face_simple.imageset/Contents.json b/Modules/DesignKit/Resources/Images.xcassets/robot_face_simple.imageset/Contents.json new file mode 100644 index 0000000000..2f0a6a1a78 --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/robot_face_simple.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "robot_face_simple.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/DesignKit/Resources/Images.xcassets/robot_face_simple.imageset/robot_face_simple.svg b/Modules/DesignKit/Resources/Images.xcassets/robot_face_simple.imageset/robot_face_simple.svg new file mode 100644 index 0000000000..9ae5e10feb --- /dev/null +++ b/Modules/DesignKit/Resources/Images.xcassets/robot_face_simple.imageset/robot_face_simple.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be6c0dbe42f4ad788b955040166c851d448754de7d5075fd3b641d4384741518 +size 691 diff --git a/Modules/DesignKit/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Modules/DesignKit/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/DesignKit/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Reinforcers.xcassets/Contents.json b/Modules/DesignKit/Resources/Reinforcers.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/DesignKit/Resources/Reinforcers.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/CoreUI/Resources/Assets.xcassets/leka-logo.imageset/Contents.json b/Modules/DesignKit/Resources/Reinforcers.xcassets/fire.imageset/Contents.json similarity index 88% rename from Modules/CoreUI/Resources/Assets.xcassets/leka-logo.imageset/Contents.json rename to Modules/DesignKit/Resources/Reinforcers.xcassets/fire.imageset/Contents.json index ee7e56ebee..22c6560a7e 100644 --- a/Modules/CoreUI/Resources/Assets.xcassets/leka-logo.imageset/Contents.json +++ b/Modules/DesignKit/Resources/Reinforcers.xcassets/fire.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "leka-logo.svg", + "filename" : "fire.svg", "idiom" : "universal", "scale" : "1x" }, diff --git a/Modules/DesignKit/Resources/Reinforcers.xcassets/fire.imageset/fire.svg b/Modules/DesignKit/Resources/Reinforcers.xcassets/fire.imageset/fire.svg new file mode 100644 index 0000000000..d5bfe6636b --- /dev/null +++ b/Modules/DesignKit/Resources/Reinforcers.xcassets/fire.imageset/fire.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7293e3337957d1cee5947c4a03784e9f6614c3935b9cdb5ae185416870adb9d2 +size 2016 diff --git a/Modules/DesignKit/Resources/Reinforcers.xcassets/rainbow.imageset/Contents.json b/Modules/DesignKit/Resources/Reinforcers.xcassets/rainbow.imageset/Contents.json new file mode 100644 index 0000000000..b4ed0e6d45 --- /dev/null +++ b/Modules/DesignKit/Resources/Reinforcers.xcassets/rainbow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "rainbow.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Reinforcers.xcassets/rainbow.imageset/rainbow.svg b/Modules/DesignKit/Resources/Reinforcers.xcassets/rainbow.imageset/rainbow.svg new file mode 100644 index 0000000000..e41df8a1ea --- /dev/null +++ b/Modules/DesignKit/Resources/Reinforcers.xcassets/rainbow.imageset/rainbow.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12c6a9d65a739cb0dd6fe2ae45fca3d18d951c06d08c381147073a33440591a6 +size 2120 diff --git a/Modules/DesignKit/Resources/Reinforcers.xcassets/spinBlinkBlueViolet.imageset/Contents.json b/Modules/DesignKit/Resources/Reinforcers.xcassets/spinBlinkBlueViolet.imageset/Contents.json new file mode 100644 index 0000000000..ab8de254e3 --- /dev/null +++ b/Modules/DesignKit/Resources/Reinforcers.xcassets/spinBlinkBlueViolet.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "spinBlinkBlueViolet.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Reinforcers.xcassets/spinBlinkBlueViolet.imageset/spinBlinkBlueViolet.svg b/Modules/DesignKit/Resources/Reinforcers.xcassets/spinBlinkBlueViolet.imageset/spinBlinkBlueViolet.svg new file mode 100644 index 0000000000..50ce37d59b --- /dev/null +++ b/Modules/DesignKit/Resources/Reinforcers.xcassets/spinBlinkBlueViolet.imageset/spinBlinkBlueViolet.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9424e2f561077422968f7968143cc53ad900fd9019a469fe24d92759438c5e60 +size 4933 diff --git a/Modules/DesignKit/Resources/Reinforcers.xcassets/spinBlinkGreenOff.imageset/Contents.json b/Modules/DesignKit/Resources/Reinforcers.xcassets/spinBlinkGreenOff.imageset/Contents.json new file mode 100644 index 0000000000..ebbbe20777 --- /dev/null +++ b/Modules/DesignKit/Resources/Reinforcers.xcassets/spinBlinkGreenOff.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "spinBlinkGreenOff.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Reinforcers.xcassets/spinBlinkGreenOff.imageset/spinBlinkGreenOff.svg b/Modules/DesignKit/Resources/Reinforcers.xcassets/spinBlinkGreenOff.imageset/spinBlinkGreenOff.svg new file mode 100644 index 0000000000..7e260d50da --- /dev/null +++ b/Modules/DesignKit/Resources/Reinforcers.xcassets/spinBlinkGreenOff.imageset/spinBlinkGreenOff.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0fa5f6ec28b5f7f8727afed5922766ca4f033747cec6e0c34694a21eeb95cf08 +size 4946 diff --git a/Modules/DesignKit/Resources/Reinforcers.xcassets/sprinkles.imageset/Contents.json b/Modules/DesignKit/Resources/Reinforcers.xcassets/sprinkles.imageset/Contents.json new file mode 100644 index 0000000000..a886247e7c --- /dev/null +++ b/Modules/DesignKit/Resources/Reinforcers.xcassets/sprinkles.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "sprinkles.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/DesignKit/Resources/Reinforcers.xcassets/sprinkles.imageset/sprinkles.svg b/Modules/DesignKit/Resources/Reinforcers.xcassets/sprinkles.imageset/sprinkles.svg new file mode 100644 index 0000000000..f4c9463d4a --- /dev/null +++ b/Modules/DesignKit/Resources/Reinforcers.xcassets/sprinkles.imageset/sprinkles.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fc67778182ce47cc2dcbe8a985f90f00bebe38281e4eb3f3c599ad4720f90d9 +size 8664 diff --git a/Modules/DesignKit/Sources/Colors+DynamicInit.swift b/Modules/DesignKit/Sources/Colors+DynamicInit.swift new file mode 100644 index 0000000000..fc7f7a61a3 --- /dev/null +++ b/Modules/DesignKit/Sources/Colors+DynamicInit.swift @@ -0,0 +1,72 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +#if canImport(AppKit) + import AppKit +#endif + +#if canImport(UIKit) + import UIKit +#endif + +public extension Color { + init(light: Color, dark: Color) { + #if canImport(UIKit) + self.init(light: UIColor(light), dark: UIColor(dark)) + #else + self.init(light: NSColor(light), dark: NSColor(dark)) + #endif + } + + #if canImport(UIKit) + init(light: UIColor, dark: UIColor) { + #if os(watchOS) + // watchOS does not support light mode / dark mode + // Per Apple HIG, prefer dark-style interfaces + self.init(uiColor: dark) + #else + self.init(uiColor: UIColor(dynamicProvider: { traits in + switch traits.userInterfaceStyle { + case .light, + .unspecified: + return light + + case .dark: + return dark + + @unknown default: + assertionFailure("Unknown userInterfaceStyle: \(traits.userInterfaceStyle)") + return light + } + })) + #endif + } + #endif + + #if canImport(AppKit) + init(light: NSColor, dark: NSColor) { + self.init(nsColor: NSColor(name: nil, dynamicProvider: { appearance in + switch appearance.name { + case .aqua, + .vibrantLight, + .accessibilityHighContrastAqua, + .accessibilityHighContrastVibrantLight: + return light + + case .darkAqua, + .vibrantDark, + .accessibilityHighContrastDarkAqua, + .accessibilityHighContrastVibrantDark: + return dark + + default: + assertionFailure("Unknown appearance: \(appearance.name)") + return light + } + })) + } + #endif +} diff --git a/Modules/DesignKit/Sources/Colors.swift b/Modules/DesignKit/Sources/Colors.swift new file mode 100644 index 0000000000..19800c150c --- /dev/null +++ b/Modules/DesignKit/Sources/Colors.swift @@ -0,0 +1,20 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +public extension Color { + static let lkBackground: Color = .init(uiColor: .systemGray6) + static let lkStroke: Color = .init(uiColor: .systemGray) + static let lkNavigationTitle: Color = DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor + static let lkGreen: Color = .init(light: UIColor(displayP3Red: 175 / 255, green: 206 / 255, blue: 54 / 255, alpha: 1), + dark: UIColor(displayP3Red: 175 / 255, green: 206 / 255, blue: 54 / 255, alpha: 1)) +} + +public extension ShapeStyle where Self == Color { + static var lkBackground: Color { .lkBackground } + static var lkStroke: Color { .lkStroke } + static var lkNavigationTitle: Color { .lkNavigationTitle } + static var lkGreen: Color { .lkGreen } +} diff --git a/Modules/DesignKit/Sources/LottieView.swift b/Modules/DesignKit/Sources/LottieView.swift new file mode 100644 index 0000000000..0d831504a4 --- /dev/null +++ b/Modules/DesignKit/Sources/LottieView.swift @@ -0,0 +1,24 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Lottie +import SwiftUI + +public extension LottieView { + init( + animation: LottieAnimation, + speed: CGFloat, + loopMode: LottieLoopMode = .playOnce, + _ completion: LottieCompletionBlock? = nil + ) where Placeholder == EmptyView { + let view = Lottie.LottieView(animation: animation) + .configure { + $0.loopMode = loopMode + $0.animationSpeed = speed + $0.play(completion: completion) + } + + self = view + } +} diff --git a/Modules/DesignKit/Sources/Modifiers/AlertWhenNoUserSelected.swift b/Modules/DesignKit/Sources/Modifiers/AlertWhenNoUserSelected.swift new file mode 100644 index 0000000000..ff577545ce --- /dev/null +++ b/Modules/DesignKit/Sources/Modifiers/AlertWhenNoUserSelected.swift @@ -0,0 +1,64 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// MARK: - AlertWhenNoUserSelected + +struct AlertWhenNoUserSelected: ViewModifier { + // MARK: Lifecycle + + public init() { + // nothing to do + } + + // MARK: Internal + + func body(content: Content) -> some View { + content + .onAppear { + self.showAlert = true + } + .alert("Aucun utilisateur sélectionné pour cette activité.", isPresented: self.$showAlert) { + self.alertContent + } message: { + Text("Avant de commencer l'activité, sélectionnez un utilisateur.") + } + } + + // MARK: Private + + @State private var showAlert: Bool = false + + private var alertContent: some View { + Group { + Button( + role: .destructive, + action: { + self.showAlert.toggle() + }, + label: { + Text("Continuer sans utilisateur") + } + ) + Button( + role: .none, + action: { + self.showAlert.toggle() + }, + label: { + Text("Sélectionner un utilisateur") + .font(.system(size: 17, weight: .semibold)) + .foregroundColor(.accentColor) + } + ) + } + } +} + +public extension View { + func alertWhenNoUserSelected() -> some View { + modifier(AlertWhenNoUserSelected()) + } +} diff --git a/Modules/DesignKit/Sources/Modifiers/AlertWhenRobotIsNeeded.swift b/Modules/DesignKit/Sources/Modifiers/AlertWhenRobotIsNeeded.swift new file mode 100644 index 0000000000..ed35227d38 --- /dev/null +++ b/Modules/DesignKit/Sources/Modifiers/AlertWhenRobotIsNeeded.swift @@ -0,0 +1,64 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// MARK: - AlertWhenRobotIsNeeded + +struct AlertWhenRobotIsNeeded: ViewModifier { + // MARK: Lifecycle + + public init() { + // nothing to do + } + + // MARK: Internal + + func body(content: Content) -> some View { + content + .onAppear { + self.showAlert = true + } + .alert("Cette activité nécessite l'utilisation du robot !", isPresented: self.$showAlert) { + self.alertContent + } message: { + Text("Avant de commencer l'activité, connectez-vous en Bluetooth à votre robot.") + } + } + + // MARK: Private + + @State private var showAlert: Bool = false + + private var alertContent: some View { + Group { + Button( + role: .destructive, + action: { + self.showAlert.toggle() + }, + label: { + Text("Continuer sans le robot") + } + ) + Button( + role: .none, + action: { + self.showAlert.toggle() + }, + label: { + Text("Se connecter") + .font(.system(size: 17, weight: .semibold)) + .foregroundColor(.accentColor) + } + ) + } + } +} + +public extension View { + func alertWhenRobotIsNeeded() -> some View { + modifier(AlertWhenRobotIsNeeded()) + } +} diff --git a/Modules/DesignKit/Sources/StyleManager.swift b/Modules/DesignKit/Sources/StyleManager.swift new file mode 100644 index 0000000000..1caa1b8252 --- /dev/null +++ b/Modules/DesignKit/Sources/StyleManager.swift @@ -0,0 +1,37 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +public class StyleManager: ObservableObject { + // MARK: Lifecycle + + public init(accentColor: Color? = nil, colorScheme: ColorScheme = .light) { + self.accentColor = accentColor + self.colorScheme = colorScheme + } + + // MARK: Public + + public static let shared = StyleManager(accentColor: DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor) + + @Published public var accentColor: Color? + @Published public var colorScheme: ColorScheme + + public func setDefaultColorScheme(_ colorScheme: ColorScheme) { + self.colorScheme = colorScheme + } + + public func toggleAccentColor() { + self.accentColor = if self.accentColor == nil { + DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor + } else { + nil + } + } + + public func toggleColorScheme() { + self.colorScheme = self.colorScheme == .light ? .dark : .light + } +} diff --git a/Modules/DesignKit/Sources/Views/LekaLogo.swift b/Modules/DesignKit/Sources/Views/LekaLogo.swift new file mode 100644 index 0000000000..89191fde55 --- /dev/null +++ b/Modules/DesignKit/Sources/Views/LekaLogo.swift @@ -0,0 +1,55 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +public struct LekaLogo: View { + // MARK: Lifecycle + + public init(width: CGFloat? = nil, height: CGFloat? = nil) { + self.width = width + self.height = height + } + + // MARK: Public + + public var body: some View { + Image( + DesignKitAsset.Assets.lekaLogo.name, + bundle: DesignKitResources.bundle + ) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: self.width, height: self.height) + } + + // MARK: Private + + private let width: CGFloat? + private let height: CGFloat? +} + +#Preview { + VStack { + LekaLogo() + + Divider() + .padding() + .hidden() + + LekaLogo(width: 80) + LekaLogo(width: 100) + LekaLogo(width: 120) + LekaLogo(width: 200) + + Divider() + .padding() + .hidden() + + LekaLogo(height: 80) + LekaLogo(height: 100) + LekaLogo(height: 120) + LekaLogo(height: 200) + } +} diff --git a/Modules/DesignKit/Sources/Views/TagView.swift b/Modules/DesignKit/Sources/Views/TagView.swift new file mode 100644 index 0000000000..a58e92a46b --- /dev/null +++ b/Modules/DesignKit/Sources/Views/TagView.swift @@ -0,0 +1,52 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +public struct TagView: View { + // MARK: Lifecycle + + public init(title: String, systemImage: String = "", action: (() -> Void)? = nil) { + self.title = title + self.systemImage = systemImage + self.action = action + } + + // MARK: Public + + public var body: some View { + Button { + self.action?() + } label: { + HStack(spacing: 12) { + Text(self.title) + + if !self.systemImage.isEmpty { + Image(systemName: self.systemImage) + .foregroundStyle(self.styleManager.accentColor!) + .font(.callout) + } + } + } + .buttonStyle(.bordered) + .foregroundStyle(.secondary) + .tint(nil) + } + + // MARK: Private + + private let title: String + private let systemImage: String + private let action: (() -> Void)? + + @ObservedObject private var styleManager: StyleManager = .shared +} + +#Preview { + VStack { + TagView(title: "Add", systemImage: "plus") + TagView(title: "Add", systemImage: "") + TagView(title: "Add") + } +} diff --git a/Modules/DesignKit/Tests/CoreUITests.swift b/Modules/DesignKit/Tests/CoreUITests.swift new file mode 100644 index 0000000000..4482884d82 --- /dev/null +++ b/Modules/DesignKit/Tests/CoreUITests.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +final class DesignKitTests: XCTestCase { + func test_example() { + XCTAssertEqual("DesignKit", "DesignKit") + } +} diff --git a/Modules/GameEngineKit/Examples/GameEngineKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Modules/GameEngineKit/Examples/GameEngineKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Modules/GameEngineKit/Examples/GameEngineKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Examples/GameEngineKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Modules/GameEngineKit/Examples/GameEngineKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Modules/GameEngineKit/Examples/GameEngineKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Examples/GameEngineKitExample/Resources/Assets.xcassets/Contents.json b/Modules/GameEngineKit/Examples/GameEngineKitExample/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/GameEngineKit/Examples/GameEngineKitExample/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Examples/GameEngineKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Modules/GameEngineKit/Examples/GameEngineKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/GameEngineKit/Examples/GameEngineKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/ContentView.swift b/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/ContentView.swift new file mode 100644 index 0000000000..8a3badc36e --- /dev/null +++ b/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/ContentView.swift @@ -0,0 +1,16 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import GameEngineKit +import SwiftUI + +struct ContentView: View { + var body: some View { + Text("Hello, World") + } +} + +#Preview { + ContentView() +} diff --git a/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/MainApp.swift b/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/MainApp.swift new file mode 100644 index 0000000000..04a53e0a91 --- /dev/null +++ b/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/MainApp.swift @@ -0,0 +1,15 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import GameEngineKit +import SwiftUI + +@main +struct GameEngineKitExample: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/Modules/GameEngineKit/Project.swift b/Modules/GameEngineKit/Project.swift new file mode 100644 index 0000000000..e03620618b --- /dev/null +++ b/Modules/GameEngineKit/Project.swift @@ -0,0 +1,26 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.module( + name: "GameEngineKit", + examples: [ + ModuleExample( + name: "GameEngineKitExample" + ), + ], + dependencies: [ + .project(target: "DesignKit", path: Path("../../Modules/DesignKit")), + .project(target: "RobotKit", path: Path("../../Modules/RobotKit")), + .project(target: "ContentKit", path: Path("../../Modules/ContentKit")), + .project(target: "LocalizationKit", path: Path("../../Modules/LocalizationKit")), + .external(name: "SwiftUIJoystick"), + .external(name: "AudioKit"), + .external(name: "Fit"), + ] +) diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Modules/GameEngineKit/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Modules/GameEngineKit/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/Contents.json b/Modules/GameEngineKit/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/danceFreeze.imageset/Contents.json b/Modules/GameEngineKit/Resources/Assets.xcassets/danceFreeze.imageset/Contents.json new file mode 100644 index 0000000000..e2e8598974 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/danceFreeze.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "danceFreeze.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/danceFreeze.imageset/danceFreeze.png b/Modules/GameEngineKit/Resources/Assets.xcassets/danceFreeze.imageset/danceFreeze.png new file mode 100644 index 0000000000..b0565c5bcf --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/danceFreeze.imageset/danceFreeze.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8728b6e42bf7c5cb3401749a82f7bd2c8464c6317f6fe710a428501d53e9f192 +size 76397 diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/hideAndSeek.imageset/Contents.json b/Modules/GameEngineKit/Resources/Assets.xcassets/hideAndSeek.imageset/Contents.json new file mode 100644 index 0000000000..634edcb341 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/hideAndSeek.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Hide-and-seek.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/hideAndSeek.imageset/Hide-and-seek.svg b/Modules/GameEngineKit/Resources/Assets.xcassets/hideAndSeek.imageset/Hide-and-seek.svg new file mode 100644 index 0000000000..f908fb75d3 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/hideAndSeek.imageset/Hide-and-seek.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ba94426eb4ca07e950d64edd301004852364a3d24413b525afce687c1e53188 +size 10146 diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/pictogram_leka_light.imageset/Contents.json b/Modules/GameEngineKit/Resources/Assets.xcassets/pictogram_leka_light.imageset/Contents.json new file mode 100644 index 0000000000..51973326d8 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/pictogram_leka_light.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "pictogram_leka_light.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/pictogram_leka_light.imageset/pictogram_leka_light.svg b/Modules/GameEngineKit/Resources/Assets.xcassets/pictogram_leka_light.imageset/pictogram_leka_light.svg new file mode 100644 index 0000000000..13093d2cb7 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/pictogram_leka_light.imageset/pictogram_leka_light.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c7d37582485e4925fc775acd907a6982d0ba2062049ec8d8a2e913a8b804723 +size 1076 diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/pictogram_leka_motion.imageset/Contents.json b/Modules/GameEngineKit/Resources/Assets.xcassets/pictogram_leka_motion.imageset/Contents.json new file mode 100644 index 0000000000..db3b8faaa6 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/pictogram_leka_motion.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "pictogram_leka_motion.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/pictogram_leka_motion.imageset/pictogram_leka_motion.svg b/Modules/GameEngineKit/Resources/Assets.xcassets/pictogram_leka_motion.imageset/pictogram_leka_motion.svg new file mode 100644 index 0000000000..c4c2b355a9 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/pictogram_leka_motion.imageset/pictogram_leka_motion.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbeb14de1398693fd00d69adc17cb627e1233a6eed1eb7f9d26d1ca90bdf4b1c +size 1190 diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-1.imageset/Contents.json b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-1.imageset/Contents.json new file mode 100644 index 0000000000..f81484d0ba --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-1.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "reinforcer-1-green-spin.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-1.imageset/reinforcer-1-green-spin.svg b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-1.imageset/reinforcer-1-green-spin.svg new file mode 100644 index 0000000000..7e260d50da --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-1.imageset/reinforcer-1-green-spin.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0fa5f6ec28b5f7f8727afed5922766ca4f033747cec6e0c34694a21eeb95cf08 +size 4946 diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-2.imageset/Contents.json b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-2.imageset/Contents.json new file mode 100644 index 0000000000..cdec187cdb --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-2.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "reinforcer-2-violet_green_blink-spin.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-2.imageset/reinforcer-2-violet_green_blink-spin.svg b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-2.imageset/reinforcer-2-violet_green_blink-spin.svg new file mode 100644 index 0000000000..50ce37d59b --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-2.imageset/reinforcer-2-violet_green_blink-spin.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9424e2f561077422968f7968143cc53ad900fd9019a469fe24d92759438c5e60 +size 4933 diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-3.imageset/Contents.json b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-3.imageset/Contents.json new file mode 100644 index 0000000000..c998581654 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-3.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "reinforcer-3-fire-static.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-3.imageset/reinforcer-3-fire-static.svg b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-3.imageset/reinforcer-3-fire-static.svg new file mode 100644 index 0000000000..d5bfe6636b --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-3.imageset/reinforcer-3-fire-static.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7293e3337957d1cee5947c4a03784e9f6614c3935b9cdb5ae185416870adb9d2 +size 2016 diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-4.imageset/Contents.json b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-4.imageset/Contents.json new file mode 100644 index 0000000000..78b8330b94 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-4.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "reinforcer-4-glitters-static.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-4.imageset/reinforcer-4-glitters-static.svg b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-4.imageset/reinforcer-4-glitters-static.svg new file mode 100644 index 0000000000..f4c9463d4a --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-4.imageset/reinforcer-4-glitters-static.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fc67778182ce47cc2dcbe8a985f90f00bebe38281e4eb3f3c599ad4720f90d9 +size 8664 diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-5.imageset/Contents.json b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-5.imageset/Contents.json new file mode 100644 index 0000000000..6bc2f4e0e0 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-5.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "reinforcer-5-rainbow-static.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-5.imageset/reinforcer-5-rainbow-static.svg b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-5.imageset/reinforcer-5-rainbow-static.svg new file mode 100644 index 0000000000..e41df8a1ea --- /dev/null +++ b/Modules/GameEngineKit/Resources/Assets.xcassets/reinforcer-5.imageset/reinforcer-5-rainbow-static.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12c6a9d65a739cb0dd6fe2ae45fca3d18d951c06d08c381147073a33440591a6 +size 2120 diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/Contents.json b/Modules/GameEngineKit/Resources/Exercises.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/Contents.json b/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/icon-motion_mode-movement.imageset/Contents.json b/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/icon-motion_mode-movement.imageset/Contents.json new file mode 100644 index 0000000000..2fdf65d7a7 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/icon-motion_mode-movement.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "dance_freeze-icon-motion_mode-movement.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/icon-motion_mode-movement.imageset/dance_freeze-icon-motion_mode-movement.svg b/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/icon-motion_mode-movement.imageset/dance_freeze-icon-motion_mode-movement.svg new file mode 100644 index 0000000000..f26d2ff43b --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/icon-motion_mode-movement.imageset/dance_freeze-icon-motion_mode-movement.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09a72c8c27a2e5fe620cb9c5420f195d6a7a9cf4d5a2aed8238935454b61dfe0 +size 1447 diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/icon-motion_mode-rotation.imageset/Contents.json b/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/icon-motion_mode-rotation.imageset/Contents.json new file mode 100644 index 0000000000..a890b0a861 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/icon-motion_mode-rotation.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "dance_freeze-icon-motion_mode-rotation.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/icon-motion_mode-rotation.imageset/dance_freeze-icon-motion_mode-rotation.svg b/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/icon-motion_mode-rotation.imageset/dance_freeze-icon-motion_mode-rotation.svg new file mode 100644 index 0000000000..6ac573560f --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/icon-motion_mode-rotation.imageset/dance_freeze-icon-motion_mode-rotation.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f64a27c934d7b9cab544740caf20ac5e48510843240e166d02babedf2ffbc4f +size 2516 diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/image-illustration.imageset/Contents.json b/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/image-illustration.imageset/Contents.json new file mode 100644 index 0000000000..ac08b2a30d --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/image-illustration.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "dance_freeze-image-illustration.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/image-illustration.imageset/dance_freeze-image-illustration.svg b/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/image-illustration.imageset/dance_freeze-image-illustration.svg new file mode 100644 index 0000000000..dd17635819 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/DanceFreeze/image-illustration.imageset/dance_freeze-image-illustration.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10369bfa46eef6ffc60feed1a67ffab0adeda579f8dd3973d3f757397750860f +size 6330 diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/Contents.json b/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/icon-stimulation-light.imageset/Contents.json b/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/icon-stimulation-light.imageset/Contents.json new file mode 100644 index 0000000000..0a656bab5c --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/icon-stimulation-light.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon-stimulation-light.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/icon-stimulation-light.imageset/icon-stimulation-light.svg b/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/icon-stimulation-light.imageset/icon-stimulation-light.svg new file mode 100644 index 0000000000..13093d2cb7 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/icon-stimulation-light.imageset/icon-stimulation-light.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c7d37582485e4925fc775acd907a6982d0ba2062049ec8d8a2e913a8b804723 +size 1076 diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/icon-stimulation-motion.imageset/Contents.json b/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/icon-stimulation-motion.imageset/Contents.json new file mode 100644 index 0000000000..f8810b37cb --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/icon-stimulation-motion.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon-stimulation-motion.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/icon-stimulation-motion.imageset/icon-stimulation-motion.svg b/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/icon-stimulation-motion.imageset/icon-stimulation-motion.svg new file mode 100644 index 0000000000..c4c2b355a9 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/icon-stimulation-motion.imageset/icon-stimulation-motion.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbeb14de1398693fd00d69adc17cb627e1233a6eed1eb7f9d26d1ca90bdf4b1c +size 1190 diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/image-illustration.imageset/Contents.json b/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/image-illustration.imageset/Contents.json new file mode 100644 index 0000000000..ac9f39f431 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/image-illustration.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "image-illustration.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/image-illustration.imageset/image-illustration.svg b/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/image-illustration.imageset/image-illustration.svg new file mode 100644 index 0000000000..f908fb75d3 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/HideAndSeek/image-illustration.imageset/image-illustration.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ba94426eb4ca07e950d64edd301004852364a3d24413b525afce687c1e53188 +size 10146 diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/Contents.json b/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/icon-keyboard-full.imageset/Contents.json b/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/icon-keyboard-full.imageset/Contents.json new file mode 100644 index 0000000000..94746e2568 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/icon-keyboard-full.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon-keyboard-full.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/icon-keyboard-full.imageset/icon-keyboard-full.svg b/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/icon-keyboard-full.imageset/icon-keyboard-full.svg new file mode 100644 index 0000000000..dff5ce4815 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/icon-keyboard-full.imageset/icon-keyboard-full.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e65ed3375be530a5501c319f8be0fb3722770e315162cae336808d072ea094ee +size 2712 diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/icon-keyboard-partial.imageset/Contents.json b/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/icon-keyboard-partial.imageset/Contents.json new file mode 100644 index 0000000000..d3b0d57f9d --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/icon-keyboard-partial.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon-keyboard-partial.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/icon-keyboard-partial.imageset/icon-keyboard-partial.svg b/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/icon-keyboard-partial.imageset/icon-keyboard-partial.svg new file mode 100644 index 0000000000..abba8bd105 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/icon-keyboard-partial.imageset/icon-keyboard-partial.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:282d7402eb1269e497e676d0d64811e200f46324eb7c6139267c8c872b9f28ad +size 4145 diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/image-illustration.imageset/Contents.json b/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/image-illustration.imageset/Contents.json new file mode 100644 index 0000000000..ac9f39f431 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/image-illustration.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "image-illustration.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/image-illustration.imageset/image-illustration.svg b/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/image-illustration.imageset/image-illustration.svg new file mode 100644 index 0000000000..44d067f820 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Exercises.xcassets/Melody/image-illustration.imageset/image-illustration.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90e5bfcf6d9f9481322aa9f44318fe38c48487e2473be251bf5fc5a4a11f3a56 +size 10016 diff --git a/Modules/GameEngineKit/Resources/Localizable.xcstrings b/Modules/GameEngineKit/Resources/Localizable.xcstrings new file mode 100644 index 0000000000..d1f3820dfd --- /dev/null +++ b/Modules/GameEngineKit/Resources/Localizable.xcstrings @@ -0,0 +1,564 @@ +{ + "version": "1.0", + "sourceLanguage": "en", + "strings": { + "game_engine_kit.discover_leka_view.pause_button_label": { + "comment": "DiscoverLekaView pause button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Pause" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pause" + } + } + } + }, + "game_engine_kit.discover_leka_view.play_button_label": { + "comment": "Discover LekaView play button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Play" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jouer" + } + } + } + }, + "game_engine_kit.discover_leka_view.player.instructions": { + "comment": "DiscoverLekaView instructions", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "\"Discover Leka\" allows the accompanied person to become familiar with Leka\nbefore even entering into learning Activities and Curriculums.\nThe robot will come to life, taking pauses so that the care receiver can\ntame your new companion!" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\"D\u00e9couvre Leka\" permet \u00e0 la personne accompagn\u00e9e de se familiariser \u00e0 Leka\navant m\u00eame d'entrer dans les apprentissages des Activit\u00e9s et des Parcours.\nLe robot va s'animer en faisant des pauses afin que\nla personne accompagn\u00e9e puisse apprivoiser son nouveau compagnon !" + } + } + } + }, + "game_engine_kit.discover_leka_view.stop_button_label": { + "comment": "DiscoverLekaView stop button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Stop" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Stop" + } + } + } + }, + "gameenginekit.activity_view.continue_button": { + "comment": "The title of the continue button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Continue" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Continuer" + } + } + } + }, + "gameenginekit.activity_view.hide_reinforcer_to_show_answers_button": { + "comment": "The title of the hide reinforcer to show answers button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Review answers" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Revoir les r\u00e9ponses" + } + } + } + }, + "gameenginekit.activity_view.quit_activity_alert.cancel_button_label": { + "comment": "Quit activity alert cancel button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Cancel" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Annuler" + } + } + } + }, + "gameenginekit.activity_view.quit_activity_alert.message": { + "comment": "Quit activity alert message", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Do you want to save your progress before quitting?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Voulez-vous enregistrer vos r\u00e9sultats avant de fermer ?" + } + } + } + }, + "gameenginekit.activity_view.quit_activity_alert.quit_without_saving_button_label": { + "comment": "Quit activity alert quit without saving button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Quit Without Saving" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quitter sans Enregistrer" + } + } + } + }, + "gameenginekit.activity_view.quit_activity_alert.save_quit_button_label": { + "comment": "Quit activity alert save and quit button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Save and Quit" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer et Quitter" + } + } + } + }, + "gameenginekit.activity_view.quit_activity_alert.title": { + "comment": "Quit activity alert title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Quit activity?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quitter l'activit\u00e9 ?" + } + } + } + }, + "gameenginekit.activity_view.toolbar.dismiss_button": { + "comment": "The title of the dismiss button", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Dismiss" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Annuler" + } + } + } + }, + "gameenginekit.success_failure_view.failure_cheering_label": { + "comment": "Success and Failure view cheering label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Try again!" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Essaies encore !" + } + } + } + }, + "gameenginekit.success_failure_view.quit_without_saving_button_label": { + "comment": "Success and Failure view quit without saving button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Quit without saving" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quitter sans Enregistrer" + } + } + } + }, + "gameenginekit.success_failure_view.save_quit_button_label": { + "comment": "Success and Failure view save & quit button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Save" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer" + } + } + } + }, + "gameenginekit.success_failure_view.success_cheering_label": { + "comment": "Success and Failure view cheering label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Well done, you've succeeded this activity!" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bien jou\u00e9, tu as r\u00e9ussi l'activit\u00e9 !" + } + } + } + }, + "gameenginekit.success_failure_view.success_percentage_label": { + "comment": "Success and Failure view success percentage label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "%.0f%% of success!" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%.0f%% de succ\u00e8s !" + } + } + } + }, + "lekaapp.dance_freeze_view.auto_button_label": { + "comment": "DanceFreezeView auto button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Play - Auto mode" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jouer - Mode auto" + } + } + } + }, + "lekaapp.dance_freeze_view.instructions": { + "comment": "DanceFreezeView instructions", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Dance with Leka to the music and act like a statue when he stops" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Danse avec Leka au rythme de la musique et fais la statue lorsque elle s'arr\u00eate" + } + } + } + }, + "lekaapp.dance_freeze_view.manual_button_label": { + "comment": "DanceFreezeView manual button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Play - Manual mode" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jouer - Mode manuel" + } + } + } + }, + "lekaapp.dance_freeze_view.movement_button_label": { + "comment": "DanceFreezeView movement button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Movement" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "D\u00e9placement" + } + } + } + }, + "lekaapp.dance_freeze_view.music_selection_title": { + "comment": "DanceFreezeView music selection title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Music selection" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "S\u00e9lection de la musique" + } + } + } + }, + "lekaapp.dance_freeze_view.rotation_button_label": { + "comment": "DanceFreezeView rotation button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Rotation" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rotation" + } + } + } + }, + "lekaapp.hide_and_seek_view.launcher.instructions": { + "comment": "HideAndSeekView Launcher instructions", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Press OK when Leka is hidden" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Appuies sur OK lorsque Leka est cach\u00e9" + } + } + } + }, + "lekaapp.hide_and_seek_view.launcher.ok_button_label": { + "comment": "HideAndSeekView Laucher OK button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Ok" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ok" + } + } + } + }, + "lekaapp.hide_and_seek_view.player.found_button_label": { + "comment": "HideAndSeekView Player Found Button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Found!" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Trouv\u00e9 !" + } + } + } + }, + "lekaapp.hide_and_seek_view.player.instructions": { + "comment": "HideAndSeekView Player instructions", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Encourage the care receiver to seek Leka.\nYou can throw a reinforcer to give him\na visual and/or audible clue.\nPress FOUND! once the robot found." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Incites la personne accompagn\u00e9e \u00e0 chercher Leka.\nTu peux lancer un renfor\u00e7ateur pour lui donner un indice visuel et/ou sonore.\nAppuies sur \"TROUV\u00c9\" une fois le robot trouv\u00e9." + } + } + } + }, + "lekaapp.melody_view.keyboard_full": { + "comment": "MelodyView keyboard full keyboard label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Full keyboard" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clavier entier" + } + } + } + }, + "lekaapp.melody_view.keyboard_partial": { + "comment": "MelodyView partial keyboard label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Partial keyboard" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clavier partiel" + } + } + } + }, + "lekaapp.melody_view.play_button_label": { + "comment": "MelodyView play button label", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Play" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jouer" + } + } + } + }, + "lekaapp.melody_view.song_selector_title": { + "comment": "MelodyView music selection title", + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "Music selection" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "S\u00e9lection de la musique" + } + } + } + } + } +} diff --git a/Modules/GameEngineKit/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Modules/GameEngineKit/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Early_Bird.mp3 b/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Early_Bird.mp3 new file mode 100755 index 0000000000..3522762e46 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Early_Bird.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf9656eb631c433ad9729ad085e667e9b8ec465a13f277f9760130a14c408d79 +size 3459953 diff --git a/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Empty_Page.mp3 b/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Empty_Page.mp3 new file mode 100755 index 0000000000..9ba5848d35 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Empty_Page.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf32c9c79fc7dc8a7f7c9f36466e3c4ae2098ab40befb8780c0ee6329e88f264 +size 2007157 diff --git a/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Giggly_Squirrel.mp3 b/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Giggly_Squirrel.mp3 new file mode 100755 index 0000000000..654c27a5f8 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Giggly_Squirrel.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f961c51717d0de88fbbf8454db50ffc19d8646b2a9426ec4431311c8dc6ba93b +size 1961940 diff --git a/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Hands_On.mp3 b/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Hands_On.mp3 new file mode 100755 index 0000000000..5d48a09ae0 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Hands_On.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:066d9ef4222aa52d9de31b6700aca5d6cf53442eb7a1b206c6da24b1fe31ad67 +size 2671715 diff --git a/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Happy_Days.mp3 b/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Happy_Days.mp3 new file mode 100755 index 0000000000..faac2479ed --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Happy_Days.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f420e8c34b7b6e887d43976c29436015fc3967958ac611db3d0466e547682ed8 +size 3768736 diff --git a/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/In_The_Game.mp3 b/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/In_The_Game.mp3 new file mode 100755 index 0000000000..b8139f8e71 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/In_The_Game.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7dee0c3f6d1dc92421ded4c233f9ce81d1d94b4eb84e2c29bc796441ed485032 +size 3540931 diff --git a/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Little_by_little.mp3 b/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Little_by_little.mp3 new file mode 100755 index 0000000000..bdf2f310a3 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/DanceFreeze/Little_by_little.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebe235348bc17e68864e0b32e92b6763f5b4eab6754af423b188b58859ac5851 +size 3331314 diff --git a/Modules/GameEngineKit/Resources/Sounds/MIDISong/A_Green_Mouse.mid b/Modules/GameEngineKit/Resources/Sounds/MIDISong/A_Green_Mouse.mid new file mode 100644 index 0000000000..95268828a3 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/MIDISong/A_Green_Mouse.mid @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:531cd0b864eefbe59de8305f9893fc5987ab18261e6caf73323a4a562452e2e2 +size 587 diff --git a/Modules/GameEngineKit/Resources/Sounds/MIDISong/AuClairDeLaLune.mid b/Modules/GameEngineKit/Resources/Sounds/MIDISong/AuClairDeLaLune.mid new file mode 100644 index 0000000000..0009ffadf9 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/MIDISong/AuClairDeLaLune.mid @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:162c4ab7f3747f1d408c5744a32e7460dd0088b01ea2ff5f5a390942a3d2ec4c +size 277 diff --git a/Modules/GameEngineKit/Resources/Sounds/MIDISong/Happy_Birthday.mid b/Modules/GameEngineKit/Resources/Sounds/MIDISong/Happy_Birthday.mid new file mode 100644 index 0000000000..394638a89f --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/MIDISong/Happy_Birthday.mid @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9fed1d9a0fccdde9b05a9f40f10b653271a8c446905730f0d971e0ef6c05aec +size 257 diff --git a/Modules/GameEngineKit/Resources/Sounds/MIDISong/London_Bridge_Is_Falling_Down.mid b/Modules/GameEngineKit/Resources/Sounds/MIDISong/London_Bridge_Is_Falling_Down.mid new file mode 100644 index 0000000000..8fd146eb65 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/MIDISong/London_Bridge_Is_Falling_Down.mid @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25d0587079cbc88b30fb31cda9023e0bc708cf767973c394e3e4a9d689cf5166 +size 557 diff --git a/Modules/GameEngineKit/Resources/Sounds/MIDISong/Oh_The_Crocodiles.mid b/Modules/GameEngineKit/Resources/Sounds/MIDISong/Oh_The_Crocodiles.mid new file mode 100644 index 0000000000..6d1be0297d --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/MIDISong/Oh_The_Crocodiles.mid @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b606bfb4a3a53881b0e61570fc62526373bf65e9f2d32a228fa5b4f1fcef7e7e +size 976 diff --git a/Modules/GameEngineKit/Resources/Sounds/MIDISong/Twinkle_Twinkle_Little_Star.mid b/Modules/GameEngineKit/Resources/Sounds/MIDISong/Twinkle_Twinkle_Little_Star.mid new file mode 100644 index 0000000000..be925dd28d --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/MIDISong/Twinkle_Twinkle_Little_Star.mid @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c7aba5f5d761b1122fa49e8e24e8b9b35978d7a10a6e010b85a93ea23d7f247 +size 502 diff --git a/Modules/GameEngineKit/Resources/Sounds/MIDISong/Under_The_Moonlight.mid b/Modules/GameEngineKit/Resources/Sounds/MIDISong/Under_The_Moonlight.mid new file mode 100644 index 0000000000..3e3c3cfe33 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/MIDISong/Under_The_Moonlight.mid @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbbe7aeea57116758befa98af254b9d6cdda0814d3ef103cef3e941dc60e88db +size 159 diff --git a/Modules/GameEngineKit/Resources/Sounds/Misc/drums.mp3 b/Modules/GameEngineKit/Resources/Sounds/Misc/drums.mp3 new file mode 100644 index 0000000000..7919b39294 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Misc/drums.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ccdff66f16e0d9f06043e12628a91bed971ac6c4ead95d54a9de0a1937e8e3d7 +size 139502 diff --git a/Modules/GameEngineKit/Resources/Sounds/Misc/flute.mp3 b/Modules/GameEngineKit/Resources/Sounds/Misc/flute.mp3 new file mode 100644 index 0000000000..070bf79ee9 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Misc/flute.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfed872f91cc33aa3cacd74fc208d7aa4580c1f49aeaa4cf090b775e3d5b3b96 +size 156633 diff --git a/Modules/GameEngineKit/Resources/Sounds/Misc/guitar.mp3 b/Modules/GameEngineKit/Resources/Sounds/Misc/guitar.mp3 new file mode 100644 index 0000000000..8171f91525 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Misc/guitar.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5a7bf4100efadf54bf838ad7ac6e356a52ae3e9f553b8057b3c2527136007d4 +size 34936 diff --git a/Modules/GameEngineKit/Resources/Sounds/Misc/harmonica.mp3 b/Modules/GameEngineKit/Resources/Sounds/Misc/harmonica.mp3 new file mode 100644 index 0000000000..8695ac6ad9 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Misc/harmonica.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbfbd8f6989df7f491ab49c1dc8b4627fd3687eb5cf09deee2ee2e2512a3b339 +size 189529 diff --git a/Modules/GameEngineKit/Resources/Sounds/Misc/piano.mp3 b/Modules/GameEngineKit/Resources/Sounds/Misc/piano.mp3 new file mode 100644 index 0000000000..cad14656f6 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Misc/piano.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9191bf1c5e0790d0b6a88f90fd8480def7b2be3d46dbfe557158c7ed65c19c34 +size 156850 diff --git a/Modules/GameEngineKit/Resources/Sounds/Misc/saxophone.mp3 b/Modules/GameEngineKit/Resources/Sounds/Misc/saxophone.mp3 new file mode 100644 index 0000000000..d5be5beab7 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Misc/saxophone.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2722dfa85d39d1250523c9a1afce49d858c015b5d10765b4e2929bb968efeea +size 241262 diff --git a/Modules/GameEngineKit/Resources/Sounds/Misc/violin.mp3 b/Modules/GameEngineKit/Resources/Sounds/Misc/violin.mp3 new file mode 100644 index 0000000000..0610bdca30 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Misc/violin.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43b9e50716a6a7e0f899ec55ba59ea45fbc801eceeeebd762c24d2d0fc72be48 +size 180799 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-24-C1.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-24-C1.wav new file mode 100644 index 0000000000..e84169f9d2 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-24-C1.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:939b57c6e2570291fac15296d909c20c943d31465bec70753cc033ccc259d132 +size 1848719 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-25-C#1.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-25-C#1.wav new file mode 100644 index 0000000000..ab4005c031 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-25-C#1.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:942f27d0f31e8805dd4b1241ccc1a9f9fdb8f9904a951293467a5c13cdd4d753 +size 1221282 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-26-D1.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-26-D1.wav new file mode 100644 index 0000000000..3651c301f6 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-26-D1.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88de082b4ddcf4a92010bdbccdbd4e52daabaf799c05481401da15466a27489c +size 1472755 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-27-D#1.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-27-D#1.wav new file mode 100644 index 0000000000..c613799ec7 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-27-D#1.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a30c8644965b15941abd8f75f97fcddd2b27cc067464f7ef4cc193071519e64 +size 1830203 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-28-E1.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-28-E1.wav new file mode 100644 index 0000000000..b50475c741 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-28-E1.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:edfd3955f90ed6a541fe635a9055eb887b0338f6f9703bf225f189fa987f6a2e +size 1326017 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-29-F1.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-29-F1.wav new file mode 100644 index 0000000000..d50bf7e6cb --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-29-F1.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ae80f816d7a8e8f364ca91b595b316017ec8107e4880b946362c9f65bca78fd +size 1634347 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-30-F#1.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-30-F#1.wav new file mode 100644 index 0000000000..d623a3214f --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-30-F#1.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:921366a8b429787100cc31176195ee10df6383a32fd940051b32524dabb3b47c +size 1292925 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-31-G1.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-31-G1.wav new file mode 100644 index 0000000000..9ed441b638 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-31-G1.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49d626423895bcc5b494823cf1688ecdc837db9e1b2146ca5d248f7c45a0c39f +size 1132852 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-32-G#1.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-32-G#1.wav new file mode 100644 index 0000000000..b51b02625c --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-32-G#1.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d253f7e29d640ab8bb83ec4567c8f66c8b4bb36ebc2cd2af0c33e285ff1d12e +size 1200280 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-33-A1.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-33-A1.wav new file mode 100644 index 0000000000..ed317fe003 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-33-A1.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55e7873351e31a9076d77fce0b27ee38639aacfd57b0d168c1665411930a719e +size 1490165 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-34-A#1.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-34-A#1.wav new file mode 100644 index 0000000000..31557aa2c9 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-34-A#1.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8876d04fb8f28af44cae8fbf214e66293fa6f75eb3932bd25e51b96f71d68cee +size 1548128 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-35-B1.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-35-B1.wav new file mode 100644 index 0000000000..d74df893e4 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-35-B1.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b6fd75350024764dc557deb97536063db6a95df27213cf72934021ed9322dba +size 1449541 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-36-C2.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-36-C2.wav new file mode 100644 index 0000000000..7e95fe9747 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-36-C2.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5c1b0a3b9e860b2c0894e1b6192997d96fd81994569b4357a3436eefa05d1cf +size 1355793 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-37-C#2.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-37-C#2.wav new file mode 100644 index 0000000000..897706f24e --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-37-C#2.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2f72f2ef0330220a6ff48a843ed9f58c587d2f87f51303fbd7fb8d7267f69db +size 1100521 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-38-D2.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-38-D2.wav new file mode 100644 index 0000000000..364237065c --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-38-D2.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2796f625e9104e8f67521e7199fcc3e4695e57b369f5c1a15b176ae74c5f4cb0 +size 1079521 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-39-D#2.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-39-D#2.wav new file mode 100644 index 0000000000..47edb1c97b --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-39-D#2.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ac2e719f5126f1943c37c7e03c1ceb27d695cf63bdbe3232832c9b2306d9efc +size 893679 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-40-E2.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-40-E2.wav new file mode 100644 index 0000000000..383520a424 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-40-E2.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9232282326bd22401e24750f013a1b9b7bf9205a79fc29f8651543f31590d28a +size 802417 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-41-F2.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-41-F2.wav new file mode 100644 index 0000000000..6809f51823 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-41-F2.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ece09d9be5adfe202e24fb77d2ed365f35865f38b3ccf2db41798c8ae21a01d +size 979484 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-42-F#2.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-42-F#2.wav new file mode 100644 index 0000000000..112c5b22e1 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-42-F#2.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bae4fa308fcf950e8433f59ca7d6a0f2035acadd8c7a90be855e67430a1343cc +size 912539 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-43-G2.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-43-G2.wav new file mode 100644 index 0000000000..85b332c962 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-43-G2.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff26ea70ed77cab978efe7bbab6b3d00b73af07ff2f14615dd7d12e8bc4dc198 +size 846907 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-44-G#2.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-44-G#2.wav new file mode 100644 index 0000000000..4afb5f0ac5 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-44-G#2.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c039770cab1cc8e9cedb4918909c84220c2d6de8e4573f80328da9f7ee02e5cd +size 702449 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-45-A2.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-45-A2.wav new file mode 100644 index 0000000000..bc306842e6 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-45-A2.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8dc916dd648d40095c3b8f4e0ede961f2ed310b38a76f8bb55b367d81d2695e +size 765179 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-46-A#2.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-46-A#2.wav new file mode 100644 index 0000000000..8082ce7545 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-46-A#2.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28b5c4a759181d3d0821eaea8bc7ba7ff6e38f19363410c908a06721740a13e5 +size 691325 diff --git a/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-47-B2.wav b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-47-B2.wav new file mode 100644 index 0000000000..fb36e4d442 --- /dev/null +++ b/Modules/GameEngineKit/Resources/Sounds/Samples/Xylo-47-B2.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8005b380313cd0c09252e713c4e56637605f8ff93f65d48d6ed938d346d523a2 +size 713987 diff --git a/Modules/GameEngineKit/Resources/animations/activity_end_success.animation.lottie.json b/Modules/GameEngineKit/Resources/animations/activity_end_success.animation.lottie.json new file mode 100644 index 0000000000..26c1eee5bd --- /dev/null +++ b/Modules/GameEngineKit/Resources/animations/activity_end_success.animation.lottie.json @@ -0,0 +1 @@ +{"v":"5.7.7","fr":29.9700012207031,"ip":0,"op":101.000004113814,"w":1024,"h":768,"nm":"bravo-ae","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[512,384,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"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":[[-29.265,-49.751],[33.735,-49.751]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":1,"ml":4,"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":[88.885,99.754],"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":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":100.000004073084,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"ribbon","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[512,384,0],"ix":2,"l":2},"a":{"a":0,"k":[512,384,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":1024,"h":768,"ip":0,"op":100.000004073084,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"face","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[512,384,0],"ix":2,"l":2},"a":{"a":0,"k":[512,384,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":1024,"h":768,"ip":7.00000028511585,"op":100.000004073084,"st":7.00000028511585,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"reflexion","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.033],"y":[0]},"t":13,"s":[0]},{"t":94.0000038286985,"s":[-359]}],"ix":10},"p":{"a":0,"k":[512,384,0],"ix":2,"l":2},"a":{"a":0,"k":[512,384,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":1024,"h":768,"ip":0,"op":100.000004073084,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"yellow","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[512,351,0],"to":[0,5.5,0],"ti":[0,-5.5,0]},{"t":8.00000032584668,"s":[512,384,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.528,0.528,0.667],"y":[1,1,1]},"o":{"x":[0.554,0.554,0.333],"y":[0,0,0]},"t":0,"s":[27,27,100]},{"i":{"x":[0.298,0.298,0.667],"y":[1,1,1]},"o":{"x":[0.555,0.555,0.333],"y":[-0.01,-0.01,0]},"t":8,"s":[120,120,100]},{"t":13.0000005295009,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[163.043,163.043],"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,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.972999961703,0.905999995213,0.109999997008,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.5],"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":100.000004073084,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"ribbon","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[512,384,0],"ix":2,"l":2},"a":{"a":0,"k":[512,384,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"w":1024,"h":768,"ip":0,"op":100.000004073084,"st":0,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"ribbon","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[518.5,331.5,0],"ix":2,"l":2},"a":{"a":0,"k":[-38,-66,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[7.201,-6.951],[0,0],[-8.5,-0.5],[-3.799,4.049]],"o":[[0,0],[0,0],[-7.284,7.031],[0,0],[11.5,-1],[4.443,-4.735]],"v":[[45.5,-61],[1,-61.5],[-5,-54.5],[-24,-49.5],[19,-49],[40,-52.5]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.13,"y":0},"t":13,"s":[{"i":[[0,0],[0,0],[26.838,-36.315],[0,0],[-8.5,-0.5],[-7.638,13.049]],"o":[[0,0],[0,0],[-5.662,8.366],[0,0],[11.5,-1],[16.201,-27.678]],"v":[[87.182,-201.909],[42.682,-202.409],[-8.636,-63.136],[-24,-49.5],[19,-49],[45,-79.773]],"c":true}]},{"t":68.0000027696968,"s":[{"i":[[0,0],[0,0],[26.838,-36.315],[0,0],[-8.5,-0.5],[-7.638,13.049]],"o":[[0,0],[0,0],[-5.662,8.366],[0,0],[11.5,-1],[16.201,-27.678]],"v":[[76.182,-152.909],[31.682,-153.409],[-4.636,-68.136],[-24,-49.5],[19,-49],[48,-76.773]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"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,0.23137254902,0.188235294118,1],"ix":4},"o":{"a":0,"k":99,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[16.252,-176.494],"ix":2},"a":{"a":0,"k":[58.953,-162.445],"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":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":102.000004154545,"st":0,"bm":0}]},{"id":"comp_2","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[512.75,412.25,0],"ix":2,"l":2},"a":{"a":0,"k":[0.625,23.542,0],"ix":1,"l":2},"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":13,"s":[0,0,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":17,"s":[120,120,100]},{"t":19.0000007738859,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[23.398,23.398],"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,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[30.683,25.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":90,"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 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[23.398,23.398],"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,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-29.75,25.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":90,"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":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":100,"ix":1},"e":{"a":0,"k":50,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":95.0000038694293,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"bouche","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[513.25,432,0],"ix":2,"l":2},"a":{"a":0,"k":[1.25,48,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.717,0.717,0.717],"y":[1,1,1]},"o":{"x":[0.303,0.303,0.303],"y":[0,0,0]},"t":13,"s":[0,0,100]},{"i":{"x":[0.773,0.773,0.773],"y":[1,1,1]},"o":{"x":[0.423,0.423,0.423],"y":[0,0,0]},"t":17,"s":[120,120,100]},{"t":19.0000007738859,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-5.75,0]],"o":[[0,0],[0,0],[5.312,0]],"v":[[7,45.438],[-4.75,45.438],[1.312,50.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"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,0,0,1],"ix":4},"o":{"a":0,"k":99,"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":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":95.0000038694293,"st":0,"bm":0}]},{"id":"comp_3","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":0,"k":[512,384,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"t":13.0000005295009,"s":[90,90,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[163.043,163.043],"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,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-0.5],"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},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":13,"s":[0]},{"t":28.0000011404634,"s":[25]}],"ix":1},"e":{"a":0,"k":0,"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":100.000004073084,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[512,384,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"t":13.0000005295009,"s":[90,90,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[163.043,163.043],"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,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-0.5],"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},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":13,"s":[0]},{"t":28.0000011404634,"s":[25]}],"ix":1},"e":{"a":0,"k":0,"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":100.000004073084,"st":0,"bm":0}]},{"id":"comp_4","layers":[{"ddd":0,"ind":1,"ty":0,"nm":"feuille","refId":"comp_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":84.516,"ix":10},"p":{"a":0,"k":[541.5,566,0],"ix":2,"l":2},"a":{"a":0,"k":[21,19.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":42,"h":39,"ip":6.00000024438501,"op":96.0000039101601,"st":6.00000024438501,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"feuille","refId":"comp_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[529,547.5,0],"ix":2,"l":2},"a":{"a":0,"k":[21,19.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":42,"h":39,"ip":6.00000024438501,"op":96.0000039101601,"st":6.00000024438501,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"feuille","refId":"comp_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-11.61,"ix":10},"p":{"a":0,"k":[549,532,0],"ix":2,"l":2},"a":{"a":0,"k":[21,19.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":42,"h":39,"ip":8.00000032584668,"op":98.0000039916218,"st":8.00000032584668,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"feuille","refId":"comp_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":73.171,"ix":10},"p":{"a":0,"k":[564.5,546.5,0],"ix":2,"l":2},"a":{"a":0,"k":[21,19.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":42,"h":39,"ip":8.00000032584668,"op":98.0000039916218,"st":8.00000032584668,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"feuille","refId":"comp_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-20.861,"ix":10},"p":{"a":0,"k":[566,516.5,0],"ix":2,"l":2},"a":{"a":0,"k":[21,19.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":42,"h":39,"ip":10.0000004073083,"op":100.000004073084,"st":10.0000004073083,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"feuille","refId":"comp_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":55.873,"ix":10},"p":{"a":0,"k":[583.5,526.5,0],"ix":2,"l":2},"a":{"a":0,"k":[21,19.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":42,"h":39,"ip":10.0000004073083,"op":100.000004073084,"st":10.0000004073083,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"feuille","refId":"comp_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-16.011,"ix":10},"p":{"a":0,"k":[580,494.5,0],"ix":2,"l":2},"a":{"a":0,"k":[21,19.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":42,"h":39,"ip":12.00000048877,"op":102.000004154545,"st":12.00000048877,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"feuille","refId":"comp_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":50.186,"ix":10},"p":{"a":0,"k":[599.5,502,0],"ix":2,"l":2},"a":{"a":0,"k":[21,19.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":42,"h":39,"ip":12.00000048877,"op":102.000004154545,"st":12.00000048877,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"feuille","refId":"comp_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-23.299,"ix":10},"p":{"a":0,"k":[589.5,472.5,0],"ix":2,"l":2},"a":{"a":0,"k":[21,19.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":42,"h":39,"ip":14.0000005702317,"op":104.000004236007,"st":14.0000005702317,"bm":0},{"ddd":0,"ind":10,"ty":0,"nm":"feuille","refId":"comp_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":40.781,"ix":10},"p":{"a":0,"k":[608.5,476.5,0],"ix":2,"l":2},"a":{"a":0,"k":[21,19.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":42,"h":39,"ip":14.0000005702317,"op":104.000004236007,"st":14.0000005702317,"bm":0},{"ddd":0,"ind":11,"ty":0,"nm":"feuille","refId":"comp_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[605,447,0],"ix":2,"l":2},"a":{"a":0,"k":[21,19.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":42,"h":39,"ip":16.0000006516934,"op":106.000004317469,"st":16.0000006516934,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"branche Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[545.867,516.219,0],"ix":2,"l":2},"a":{"a":0,"k":[64.867,63.137,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-15.405,63.78]],"o":[[53.612,-3.479],[0,0]],"v":[[-57.367,55.637],[57.367,-55.637]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[64.867,63.138],"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":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":20.0000008146167,"s":[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":90.0000036657751,"st":0,"bm":0}]},{"id":"comp_5","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":0.867},"o":{"x":0.333,"y":0},"t":0,"s":[21,32.097,0],"to":[0,-0.988,0],"ti":[0,1.21,0]},{"i":{"x":0.667,"y":0.832},"o":{"x":0.333,"y":0.168},"t":14,"s":[21,22,0],"to":[0,-0.188,0],"ti":[0,1.21,0]},{"t":17.0000006924242,"s":[21,23,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.666,0.666,0.667],"y":[1,1,2.09]},"o":{"x":[0.49,0.49,0.333],"y":[0,0,0]},"t":0,"s":[9,9,100]},{"i":{"x":[0.463,0.463,0.667],"y":[1,1,1]},"o":{"x":[0.423,0.423,0.333],"y":[0,0,-0.234]},"t":14,"s":[126.877,126.877,100]},{"t":17.0000006924242,"s":[109,109,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[6.519,6.914],[-10,-0.25]],"o":[[-6.125,2.75],[11.632,0.291]],"v":[[0.625,-12.875],[-1.875,8]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[0.824]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[6]},{"i":{"x":[0.667],"y":[1.337]},"o":{"x":[0.333],"y":[1.055]},"t":6,"s":[3]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0.225]},"t":12,"s":[2.5]},{"t":16.0000006516934,"s":[3]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.709803921569,0.937254901961,0.254901960784,1],"ix":4},"o":{"a":0,"k":99,"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":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90.0000036657751,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"medal","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":2,"s":[512,0,0],"to":[0,29.747,0],"ti":[0,-48.107,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":11,"s":[512,433.454,0],"to":[0,6.339,0],"ti":[0,-3.919,0]},{"t":15.0000006109625,"s":[512,384,0]}],"ix":2,"l":2},"a":{"a":0,"k":[512,384,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":1024,"h":768,"ip":2.00000008146167,"op":102.000004154545,"st":2.00000008146167,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"laurier","refId":"comp_4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[502,384,0],"ix":2,"l":2},"a":{"a":0,"k":[512,384,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"w":1024,"h":768,"ip":21.0000008553475,"op":111.000004521123,"st":21.0000008553475,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"laurier","refId":"comp_4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[522,384,0],"ix":2,"l":2},"a":{"a":0,"k":[512,384,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":1024,"h":768,"ip":21.0000008553475,"op":111.000004521123,"st":21.0000008553475,"bm":0}],"markers":[]} diff --git a/Modules/GameEngineKit/Resources/animations/activity_end_try_again.animation.lottie.json b/Modules/GameEngineKit/Resources/animations/activity_end_try_again.animation.lottie.json new file mode 100644 index 0000000000..8ad5caff4d --- /dev/null +++ b/Modules/GameEngineKit/Resources/animations/activity_end_try_again.animation.lottie.json @@ -0,0 +1 @@ +{"v":"5.7.7","fr":29.9700012207031,"ip":0,"op":113.000004602584,"w":1024,"h":768,"nm":"try-again","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"bouche","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[144,-39,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[103.541,95.143,100],"ix":6,"l":2}},"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":[[-87.707,113.599],[-85.237,113.053]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.6,0.576470588235,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":2,"lj":1,"ml":4,"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":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":191.000007779589,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"eye","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[67,41.5,0],"ix":2,"l":2},"a":{"a":0,"k":[25,25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":50,"h":50,"ip":0,"op":190.000007738859,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"eye","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[31.5,51,0],"ix":2,"l":2},"a":{"a":0,"k":[25,25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":50,"h":50,"ip":0,"op":190.000007738859,"st":0,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25,25,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":69,"s":[41,47]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":77,"s":[41,10]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":86,"s":[41,47]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":159,"s":[41,47]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":166,"s":[41,10]},{"t":176.000007168627,"s":[41,47]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":123,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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.356,0.402],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[48,48],"ix":3},"r":{"a":0,"k":-16.353,"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":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":194.000007901782,"st":0,"bm":0}]},{"id":"comp_2","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"try again Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[670.025,309.945,0],"ix":2,"l":2},"a":{"a":0,"k":[66.025,-1.223,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":9,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[120,120,100]},{"t":18.000000733155,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.031],[0.336,-0.5],[0.437,-1.648],[0.125,-0.148],[0,-0.109],[0,-0.531],[-0.031,0],[-0.75,-0.156],[-1.555,0.094],[0.719,-1.562],[0,-0.32],[-0.188,-0.258],[-0.461,-0.094],[-0.18,0.094],[-0.836,1.82],[0,0],[0,0],[0,-0.18],[0,0],[-0.469,-1.398],[-1.609,0],[-1.555,1.281],[0,0.789],[0.703,0],[0.188,-0.125],[0.5,-0.766],[1.141,0],[0,0],[0.289,0.156],[0,2.57],[-0.219,2.07],[-0.438,0],[0,0.844],[0,0],[0.656,0],[2.562,-0.188],[-0.313,1.828],[0.687,0]],"o":[[-0.445,0],[-0.125,0.633],[-0.531,1.695],[-2.906,0.141],[-0.562,0.219],[0,0.156],[0,0.531],[0.414,0],[0,0.063],[-1.219,2.18],[0,0.367],[0.195,0.188],[0.352,-0.094],[0.695,-1.023],[0,0],[0,0],[-0.031,0.32],[0,0],[0,1.82],[0.734,1.531],[1.383,0],[2.406,-2.367],[-0.109,-0.844],[-0.25,0],[-0.469,0.422],[-1.859,2.25],[0,0],[-0.18,0],[-0.594,-0.586],[0,-1.742],[1.812,-0.156],[1.625,0],[0,0],[-0.125,-0.875],[-0.344,0],[0.219,-1.797],[-0.156,-0.875],[-0.031,0]],"v":[[-50.94,-16.047],[-52.112,-15.297],[-52.955,-11.875],[-53.94,-9.109],[-58.299,-8.734],[-59.143,-7.609],[-59.096,-7.375],[-57.971,-6.344],[-55.018,-6.484],[-56.096,-4.047],[-57.924,-0.297],[-57.643,0.641],[-56.659,1.062],[-55.862,0.781],[-53.565,-3.484],[-53.518,-3.484],[-53.518,-3.438],[-53.565,-2.688],[-53.565,-0.625],[-52.862,4.203],[-49.346,6.5],[-44.94,4.578],[-41.33,-0.156],[-42.549,-1.422],[-43.205,-1.234],[-44.659,0.547],[-49.159,3.922],[-49.44,3.922],[-50.143,3.688],[-51.034,-1.047],[-50.705,-6.766],[-47.33,-7],[-44.893,-8.266],[-44.893,-8.312],[-46.065,-9.625],[-50.424,-9.344],[-49.627,-14.781],[-50.893,-16.094]],"c":true},"ix":2},"nm":"t","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"t","np":3,"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.281,-0.062],[0,-1.352],[0,0],[-1.656,-0.789],[0,0],[-0.844,-0.18],[0,0.039],[-0.406,1.781],[-0.219,-0.516],[0.25,-1.812],[-0.438,-0.633],[-0.93,0],[-0.703,0.281],[-0.836,0.594],[0,0.875],[0.719,0],[0,0],[0.391,-0.906],[0.875,-0.117],[0,0],[0.07,0.375],[0,0],[0.062,0.398],[0.93,0.57],[0.93,0.156],[-0.031,0.727],[0.031,0.195],[1.43,0],[0,0]],"o":[[-1.156,0.523],[0,0],[0,1.43],[0,0],[0,0.664],[0.344,-0.055],[0.469,0],[1.062,0.109],[0,0.5],[0,0.773],[0.695,0.813],[0.359,0],[0.039,0],[2.281,-2.469],[-0.063,-0.844],[0,0],[-0.578,0],[-1.5,2.32],[0,0],[-0.242,0],[0,0],[0,-0.101],[-0.289,-1.055],[-0.414,-0.188],[0,-0.023],[0,-0.242],[-0.352,-1.906],[0,0],[-0.125,0]],"v":[[-43.393,-9.672],[-45.127,-6.859],[-45.127,-6.719],[-42.643,-3.391],[-43.346,-1.094],[-42.08,0.172],[-41.565,0.031],[-40.252,-2.641],[-38.33,-1.703],[-38.705,1.766],[-38.049,3.875],[-35.612,5.094],[-34.018,4.672],[-32.705,3.781],[-29.284,-1.234],[-30.455,-2.5],[-30.596,-2.5],[-32.049,-1.141],[-35.612,2.516],[-35.659,2.516],[-36.127,1.953],[-35.752,-1.422],[-35.846,-2.172],[-37.674,-4.609],[-39.69,-5.125],[-39.643,-6.25],[-39.69,-6.906],[-42.362,-9.766],[-42.784,-9.766]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-0.063,-0.406],[0,0],[0,0.25],[0,0],[0,0]],"o":[[0,0],[-0.219,-0.188],[0,0],[0,0],[0.125,0]],"v":[[-42.221,-6.625],[-42.221,-6.109],[-42.549,-6.766],[-42.549,-7],[-42.502,-7.234]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"r","np":5,"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":[[0.172,-0.25],[0.133,-3.469],[-0.594,-1.336],[-1.312,0],[0,-1.273],[-0.125,-0.602],[-1.078,0],[0,0],[-1.484,2.438],[-0.438,1.875],[-0.562,0.367],[-0.078,0.031],[0,0.742],[0.773,0],[0.297,-0.375],[1.25,-0.758],[0,1.516],[0,0],[0.875,0],[0,-0.07],[0.406,-2.062],[0.641,-1.102],[0.461,0],[0,0],[0.125,0.031],[0.039,2.227],[0,0],[-0.25,1.602],[0.281,0.32],[0.297,0]],"o":[[-0.367,0.094],[0,1.758],[0.781,1.406],[-1.312,1.633],[0,0.117],[0.422,1.062],[0,0],[1.516,0],[0.5,-0.938],[0.969,-0.445],[0.578,-0.469],[1.469,-1.445],[-0.07,-0.875],[-0.422,0],[-0.5,1.148],[0.156,-1.266],[0,0],[0,-1.469],[-0.5,0.055],[-0.281,0],[-0.641,2.086],[-0.758,1.094],[0,0],[-0.125,0],[-0.836,-0.43],[0,0],[0,-1.242],[0,-0.273],[-0.328,-0.188],[-0.391,0]],"v":[[-30.502,-6.062],[-31.252,-0.719],[-30.362,3.922],[-27.221,6.031],[-29.19,10.391],[-29.002,11.469],[-26.752,13.062],[-26.518,13.062],[-22.018,9.406],[-20.612,5.188],[-18.315,3.969],[-17.33,3.219],[-15.127,-0.062],[-16.393,-1.375],[-17.471,-0.812],[-20.096,2.047],[-19.862,-2.125],[-19.862,-4],[-21.174,-6.203],[-21.924,-6.016],[-22.955,-2.922],[-24.877,1.859],[-26.705,3.5],[-26.987,3.5],[-27.362,3.453],[-28.674,-0.531],[-28.674,-1],[-28.299,-5.266],[-28.721,-6.156],[-29.659,-6.438]],"c":true},"ix":2},"nm":"y","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-0.063,0.117],[-1.453,0.812],[0.281,-0.649],[0.602,0],[0,0],[0,0]],"o":[[0.484,-1.438],[0,0.101],[-1.024,1.937],[0,0],[0,0],[0,-0.133]],"v":[[-26.518,9.828],[-23.612,6.453],[-24.034,7.578],[-26.471,10.484],[-26.612,10.484],[-26.612,10.203]],"c":true},"ix":2},"nm":"y","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"y","np":5,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.523,-1.719],[0,-1.734],[-0.219,-0.648],[-1.656,0],[0,0],[-1.547,2.094],[0,0],[-0.344,-0.703],[-1.203,0],[-1.219,1.719],[0,0.758],[0.812,0.063],[0.25,-0.344],[0.406,-0.75],[0.586,0],[0,0],[0,1.992],[0,0],[-0.063,0.516],[0.156,0.664],[1.836,0],[0,0]],"o":[[-1.656,2.172],[0,0.508],[0.75,1.563],[0,0],[1.828,0],[0,0],[0,0.234],[0.734,1.188],[1.437,0],[1.219,-1.805],[0,-0.75],[-0.469,0],[-0.25,0.594],[-0.977,1.688],[0,0],[-0.75,-0.383],[0,0],[0,-0.297],[0,-0.211],[-0.758,-2],[0,0],[-1.57,0]],"v":[[-8.143,-4.891],[-10.627,0.969],[-10.299,2.703],[-6.69,5.047],[-6.596,5.047],[-1.534,1.906],[-1.487,1.906],[-0.971,3.312],[1.935,5.094],[5.92,2.516],[7.748,-1.328],[6.529,-2.547],[5.451,-2.031],[4.466,-0.016],[2.123,2.516],[1.795,2.516],[0.67,-1.047],[0.67,-1.938],[0.763,-3.156],[0.529,-4.469],[-3.362,-7.469],[-3.502,-7.469]],"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.375,0.805],[-1.469,0],[-0.156,-0.062],[0,-0.625],[0.219,-0.586],[0.742,-1.023],[0.578,0],[0,0],[0,0.516],[0,0]],"o":[[1.312,-2.531],[0.062,0],[0.781,0.375],[0,0.07],[-0.414,1.039],[-0.984,1.156],[0,0],[-0.719,-0.234],[0,0],[0,-0.602]],"v":[[-7.487,-1.141],[-3.315,-4.938],[-2.987,-4.844],[-1.815,-3.344],[-2.143,-2.359],[-3.877,0.734],[-6.221,2.469],[-6.971,2.469],[-8.049,1.344],[-8.049,0.969]],"c":true},"ix":2},"nm":"a","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"a","np":5,"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":[[1.469,-1.531],[0,-1.531],[-0.813,-1.109],[-0.547,0],[0,-1.219],[-0.562,-0.719],[-0.883,0],[-1.109,1.812],[-0.688,2.906],[-1.086,1.016],[0,0.602],[0,0],[0.648,0],[0,0],[0.898,-0.938],[0.461,-0.305],[0,0.508],[0,0],[0.437,0.938],[1.453,0]],"o":[[-1.5,2],[0,1.234],[0.828,0.875],[-1.438,1.437],[0,0.812],[0.648,0.75],[1.453,0],[0.687,-1.125],[1.601,-0.859],[1.156,-0.867],[0,0],[-0.164,-0.687],[0,0],[-0.383,0],[-0.789,0.664],[0.188,-1.18],[0,0],[0,-0.75],[-0.891,-1.406],[-1.625,0]],"v":[[7.49,-3.883],[5.24,1.414],[6.459,4.93],[8.521,6.242],[6.365,10.227],[7.209,12.523],[9.505,13.648],[13.349,10.93],[15.412,4.883],[19.443,2.07],[21.177,-0.133],[21.177,-0.32],[19.959,-1.352],[19.818,-1.352],[17.896,0.055],[16.021,1.508],[16.302,-1.023],[16.302,-1.539],[15.646,-4.07],[12.13,-6.18]],"c":true},"ix":2},"nm":"g","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-0.5,0.953],[-1.047,0],[0,-1.562],[0.656,-1.305],[1.117,0],[0.117,0.063],[0,0.875],[0,0]],"o":[[1.141,-1.781],[1.281,0],[0,0.508],[-1.039,1.594],[-0.164,0],[-0.875,-0.469],[0,0],[0,-0.641]],"v":[[8.568,-0.93],[11.849,-3.602],[13.771,-1.258],[12.787,1.461],[9.552,3.852],[9.13,3.758],[7.818,1.742],[7.818,1.461]],"c":true},"ix":2},"nm":"g","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[-2.281,1.555],[0.812,0],[0,0],[0.078,0.531],[0,0]],"o":[[-1,3],[0,0],[-0.297,0],[0,0],[0,-0.852]],"v":[[12.365,6.57],[9.646,11.07],[9.505,11.07],[8.943,10.273],[8.943,10.18]],"c":true},"ix":2},"nm":"g","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"g","np":6,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.523,-1.719],[0,-1.734],[-0.219,-0.648],[-1.656,0],[0,0],[-1.547,2.094],[0,0],[-0.344,-0.703],[-1.203,0],[-1.219,1.719],[0,0.758],[0.812,0.063],[0.25,-0.344],[0.406,-0.75],[0.586,0],[0,0],[0,1.992],[0,0],[-0.063,0.516],[0.156,0.664],[1.836,0],[0,0]],"o":[[-1.656,2.172],[0,0.508],[0.75,1.563],[0,0],[1.828,0],[0,0],[0,0.234],[0.734,1.188],[1.437,0],[1.219,-1.805],[0,-0.75],[-0.469,0],[-0.25,0.594],[-0.977,1.688],[0,0],[-0.75,-0.383],[0,0],[0,-0.297],[0,-0.211],[-0.758,-2],[0,0],[-1.57,0]],"v":[[21.81,-4.891],[19.326,0.969],[19.654,2.703],[23.263,5.047],[23.357,5.047],[28.42,1.906],[28.466,1.906],[28.982,3.312],[31.888,5.094],[35.873,2.516],[37.701,-1.328],[36.482,-2.547],[35.404,-2.031],[34.42,-0.016],[32.076,2.516],[31.748,2.516],[30.623,-1.047],[30.623,-1.938],[30.716,-3.156],[30.482,-4.469],[26.591,-7.469],[26.451,-7.469]],"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.375,0.805],[-1.469,0],[-0.156,-0.062],[0,-0.625],[0.219,-0.586],[0.742,-1.023],[0.578,0],[0,0],[0,0.516],[0,0]],"o":[[1.312,-2.531],[0.062,0],[0.781,0.375],[0,0.07],[-0.414,1.039],[-0.984,1.156],[0,0],[-0.719,-0.234],[0,0],[0,-0.602]],"v":[[22.466,-1.141],[26.638,-4.938],[26.966,-4.844],[28.138,-3.344],[27.81,-2.359],[26.076,0.734],[23.732,2.469],[22.982,2.469],[21.904,1.344],[21.904,0.969]],"c":true},"ix":2},"nm":"a","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"a","np":5,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.195,-1.281],[-0.031,-0.055],[-0.781,-0.086],[-0.25,0.219],[-0.133,0.766],[0.844,0.117]],"o":[[0,0.07],[0,0.602],[0.375,0],[0.305,-0.297],[0,-0.695],[-0.836,0]],"v":[[37.56,-9.062],[37.607,-8.875],[38.779,-7.844],[39.716,-8.172],[40.373,-9.766],[39.107,-10.984]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.227,-0.312],[0,-2.094],[-0.5,-0.867],[-1.406,0],[-1.399,1.312],[0,0.836],[0.812,0.133],[0.305,-0.281],[0.984,-1.164],[0.539,0],[0,0],[0.055,0.031],[0,1.219],[0,0],[-0.063,0.25],[-0.781,2.656],[0.031,0.164],[0.617,0]],"o":[[-1.031,2.563],[0,1.258],[0.812,1.312],[1.32,0],[2.094,-2.227],[0,-0.68],[-0.352,0],[-0.578,0.961],[-1.086,1.063],[0,0],[-0.102,0],[-0.813,-0.25],[0,0],[0,-0.281],[0,-0.5],[0,-0.023],[-0.164,-0.594],[-0.492,0]],"v":[[37.091,-7.047],[35.545,-0.062],[36.295,3.125],[39.623,5.094],[43.701,3.125],[46.841,-1.469],[45.623,-2.688],[44.638,-2.266],[42.295,0.922],[39.857,2.516],[39.576,2.516],[39.341,2.469],[38.123,0.266],[38.123,-0.812],[38.216,-1.609],[39.388,-6.344],[39.341,-6.625],[38.17,-7.516]],"c":true},"ix":2},"nm":"i","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"i","np":5,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.977,-1.344],[0.258,-0.969],[-0.875,-0.148],[-0.07,0],[-0.234,0.719],[-0.68,0.805],[0,0],[0.281,-1.18],[0.687,-2.328],[-0.875,-0.125],[0,0],[-0.133,0.227],[-1,1.594],[-0.625,0.172],[0,-0.383],[0,0],[0.125,-1.234],[-0.25,-0.555],[-1.531,0],[-1.656,2.062],[-0.016,0.617],[0,0.094],[0.562,0],[0.352,-0.156],[0.727,-1.211],[0.766,0],[0,0],[0,0.523],[-0.031,0.32],[0,0.453],[0,0],[0.125,0.43],[1.305,0],[0,0],[1.383,-1.812],[0.094,0.484],[1.008,0],[0,0]],"o":[[-0.992,1.625],[0,0.727],[0.055,-0.031],[0.484,0],[0.633,-1.383],[0,0],[0,0.695],[-0.188,0.828],[0,0.75],[0,0],[0.492,-0.086],[1.281,-2.906],[1.031,-1.641],[0.344,0.055],[0,0],[-0.375,2.234],[0,0.758],[0.625,1.219],[1.781,0],[1.297,-1.664],[-0.063,-0.156],[-0.25,-0.562],[-0.148,0],[-0.336,0.414],[-1.328,1.656],[0,0],[-0.438,-0.101],[0,-0.055],[0.469,-2.922],[0,0],[0,-0.352],[-0.508,-1.406],[0,0],[-1.148,0],[0,-0.109],[-0.367,-1.219],[0,0],[-0.992,0]],"v":[[46.384,-5.336],[44.509,-1.445],[45.822,-0.133],[46.009,-0.18],[47.088,-1.258],[49.056,-4.539],[49.056,-3.742],[48.634,-0.93],[47.322,3.805],[48.634,5.117],[48.681,5.117],[49.619,4.648],[53.041,-2.102],[55.525,-4.82],[56.041,-4.164],[56.041,-3.789],[55.291,1.414],[55.666,3.383],[58.9,5.211],[64.056,2.117],[66.025,-1.305],[65.931,-1.68],[64.713,-2.523],[63.963,-2.289],[62.369,0.148],[59.228,2.633],[58.478,2.633],[57.822,1.695],[57.869,1.133],[58.572,-3.93],[58.572,-4.07],[58.384,-5.242],[55.666,-7.352],[55.431,-7.352],[51.634,-4.633],[51.494,-5.523],[49.431,-7.352],[49.338,-7.352]],"c":true},"ix":2},"nm":"n","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":"n","np":3,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":95.0000038694293,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"grass","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-6,"ix":10},"p":{"a":0,"k":[692.5,564.5,0],"ix":2,"l":2},"a":{"a":0,"k":[33.5,69.5,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"w":100,"h":100,"ip":0,"op":95.0000038694293,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"grass","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":6.198,"ix":10},"p":{"a":0,"k":[696.5,564.5,0],"ix":2,"l":2},"a":{"a":0,"k":[33.5,69.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":100,"h":100,"ip":0,"op":95.0000038694293,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"poteau","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[695,561,0],"ix":2,"l":2},"a":{"a":0,"k":[0,141,0],"ix":1,"l":2},"s":{"a":0,"k":[75,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":1,"s":[12,12]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":13,"s":[12,328.429]},{"t":15.0000006109625,"s":[12,282]}],"ix":2},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[0,3],"to":[0,-18.346],"ti":[0,35.329]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":13,"s":[0,-162.569],"to":[0,-8.32],"ti":[0,4.321]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":15,"s":[0,-133],"to":[0,-22.667],"ti":[0,0]},{"t":23.0000009368092,"s":[0,-134]}],"ix":3},"r":{"a":0,"k":100,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.584313988686,0,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.089,141.486],"ix":2},"a":{"a":0,"k":[0.089,6.672],"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":"Rectangle 200","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":95.0000038694293,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"panneau","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[690.5,310.5,0],"ix":2,"l":2},"a":{"a":0,"k":[190.5,22.5,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":9,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[120,120,100]},{"t":18.000000733155,"s":[100,100,100]}],"ix":6,"l":2}},"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]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[25.8,0],[191,0],[191,45],[25.8,45],[0,22.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"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.970775008202,0.907238006592,0.10946200043,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":95.0000038694293,"st":0,"bm":0}]},{"id":"comp_3","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"grass","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[32,68.25,0],"ix":2,"l":2},"a":{"a":0,"k":[187.5,177.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0.25,-0.75],[-11.75,-8]],"o":[[-0.12,0.36],[10.499,7.148]],"v":[[197,170],[187.75,178.75]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":5,"s":[{"i":[[0.25,-0.75],[-30.75,-34.5]],"o":[[-0.12,0.36],[29.861,33.502]],"v":[[244.167,120.417],[187.75,178.75]],"c":true}]},{"t":8.00000032584668,"s":[{"i":[[0.25,-0.75],[-14.557,-10.07]],"o":[[-0.12,0.36],[33.25,23]],"v":[[231,133],[187.75,178.75]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.094990808824,0.775444240196,0,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":[186.109,177.588],"ix":2},"a":{"a":0,"k":[186.109,177.588],"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":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":97.000003950891,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"reflets 2","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.201],"y":[1]},"o":{"x":[0.155],"y":[0]},"t":3,"s":[0]},{"t":60.0000024438501,"s":[360]}],"ix":10},"p":{"a":0,"k":[-307.32,95.544,0],"ix":2,"l":2},"a":{"a":0,"k":[-306.562,97.875,0],"ix":1,"l":2},"s":{"a":0,"k":[103.784,103.784,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[39.025,0],[0,-39.025],[-39.025,0],[0,39.025]],"o":[[-39.025,0],[0,39.025],[39.025,0],[0,-39.025]],"v":[[0,-70.66],[-70.66,0],[0,70.66],[70.66,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.901960784314,0.901960784314,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-306.562,97.875],"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},{"ty":"tm","s":{"a":0,"k":75,"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":3.00000012219251,"op":193.000007861051,"st":3.00000012219251,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"reflets","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.215],"y":[1]},"o":{"x":[0.168],"y":[0]},"t":3,"s":[178]},{"t":60.0000024438501,"s":[538]}],"ix":10},"p":{"a":0,"k":[-307.32,95.12,0],"ix":2,"l":2},"a":{"a":0,"k":[-306.562,97.875,0],"ix":1,"l":2},"s":{"a":0,"k":[103.784,103.784,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[39.025,0],[0,-39.025],[-39.025,0],[0,39.025]],"o":[[-39.025,0],[0,39.025],[39.025,0],[0,-39.025]],"v":[[0,-70.66],[-70.66,0],[0,70.66],[70.66,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.901960784314,0.901960784314,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-306.562,97.875],"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},{"ty":"tm","s":{"a":0,"k":75,"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":3.00000012219251,"op":193.000007861051,"st":3.00000012219251,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"face","parent":4,"refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":3,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[-16]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":44,"s":[-14]},{"t":60.0000024438501,"s":[0]}],"ix":10},"p":{"a":0,"k":[-307.32,95.332,0],"ix":2,"l":2},"a":{"a":0,"k":[14.68,60.332,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":100,"h":100,"ip":3.00000012219251,"op":193.000007861051,"st":3.00000012219251,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"leka-ball","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":0.545},"o":{"x":0.333,"y":0},"t":3,"s":[-99.32,479.332,0],"to":[48.754,0,0],"ti":[-118.046,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0.527},"t":31,"s":[200.75,479.968,0],"to":[74.042,0,0],"ti":[-30.58,0,0]},{"t":50.0000020365418,"s":[376.68,479.332,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-307.32,95.332,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[186.625,186.625],"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,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":1,"lj":1,"ml":4,"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":[-307.32,95.332],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[87.958,87.958],"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":3.00000012219251,"op":193.000007861051,"st":3.00000012219251,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"panneau","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[511.5,385.5,0],"ix":2,"l":2},"a":{"a":0,"k":[512,384,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":1024,"h":768,"ip":48.0000019550801,"op":197.000008023975,"st":48.0000019550801,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"long line","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[484.5,380.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"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":[[-485,183.25],[536.75,183.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"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":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":194.000007901782,"st":0,"bm":0}],"markers":[]} diff --git a/Modules/GameEngineKit/Resources/animations/dance_freeze_dance.animation.lottie.json b/Modules/GameEngineKit/Resources/animations/dance_freeze_dance.animation.lottie.json new file mode 100644 index 0000000000..5a57407be7 --- /dev/null +++ b/Modules/GameEngineKit/Resources/animations/dance_freeze_dance.animation.lottie.json @@ -0,0 +1 @@ +{"v":"5.1.1","fr":29.9700012207031,"ip":0,"op":60.0000024438501,"w":1024,"h":768,"nm":"nyan-leka","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":300,"ix":10},"p":{"a":0,"k":[103.451,98.076,0],"ix":2},"a":{"a":0,"k":[4.049,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[0,0,100],"e":[20,20,100]},{"t":21.0000008553475}],"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":[[3.599,-77.61],[3.599,-200.436]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[99]},{"t":31.0000012626559}],"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":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":32,"ix":5},"lc":2,"lj":2,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":240,"ix":10},"p":{"a":0,"k":[103.451,98.076,0],"ix":2},"a":{"a":0,"k":[4.049,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[0,0,100],"e":[20,20,100]},{"t":21.0000008553475}],"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":[[3.599,-77.61],[3.599,-200.436]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[99]},{"t":31.0000012626559}],"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":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":32,"ix":5},"lc":2,"lj":2,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":0,"k":[103.451,98.076,0],"ix":2},"a":{"a":0,"k":[4.049,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[0,0,100],"e":[20,20,100]},{"t":21.0000008553475}],"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":[[3.599,-77.61],[3.599,-200.436]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[99]},{"t":31.0000012626559}],"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":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":32,"ix":5},"lc":2,"lj":2,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":120,"ix":10},"p":{"a":0,"k":[103.451,98.076,0],"ix":2},"a":{"a":0,"k":[4.049,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[0,0,100],"e":[20,20,100]},{"t":21.0000008553475}],"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":[[3.599,-77.61],[3.599,-200.436]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[99]},{"t":31.0000012626559}],"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":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":32,"ix":5},"lc":2,"lj":2,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":60,"ix":10},"p":{"a":0,"k":[103.451,98.076,0],"ix":2},"a":{"a":0,"k":[4.049,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[0,0,100],"e":[20,20,100]},{"t":21.0000008553475}],"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":[[3.599,-77.61],[3.599,-200.436]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[99]},{"t":31.0000012626559}],"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":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":32,"ix":5},"lc":2,"lj":2,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[103.451,98.076,0],"ix":2},"a":{"a":0,"k":[4.049,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[0,0,100],"e":[20,20,100]},{"t":21.0000008553475}],"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":[[3.599,-77.61],[3.599,-200.436]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[99]},{"t":31.0000012626559}],"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":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":32,"ix":5},"lc":2,"lj":2,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":0,"nm":"face","parent":4,"refId":"comp_2","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]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[-6.012],"e":[7.988]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":26,"s":[7.988],"e":[-6.012]},{"t":60.0000024438501}],"ix":10},"p":{"a":0,"k":[-307.32,95.332,0],"ix":2},"a":{"a":0,"k":[14.68,60.332,0],"ix":1},"s":{"a":0,"k":[91,91,100],"ix":6}},"ao":0,"w":100,"h":100,"ip":0,"op":300.00001221925,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"reflets 2","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":907.776,"ix":10},"p":{"a":0,"k":[-306.683,97.77,0],"ix":2},"a":{"a":0,"k":[-306.562,97.875,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":[[39.025,0],[0,-39.025],[-39.025,0],[0,39.025]],"o":[[-39.025,0],[0,39.025],[39.025,0],[0,-39.025]],"v":[[0,-70.66],[-70.66,0],[0,70.66],[70.66,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.901960784314,0.901960784314,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-306.562,97.875],"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,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":75,"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.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"reflets","parent":4,"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]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[133.315],"e":[853.315]},{"t":60.0000024438501}],"ix":10},"p":{"a":0,"k":[-307.751,95.884,0],"ix":2},"a":{"a":0,"k":[-306.562,97.875,0],"ix":1},"s":{"a":0,"k":[103.784,103.784,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[39.025,0],[0,-39.025],[-39.025,0],[0,39.025]],"o":[[-39.025,0],[0,39.025],[39.025,0],[0,-39.025]],"v":[[0,-70.66],[-70.66,0],[0,70.66],[70.66,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.901960784314,0.901960784314,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-306.562,97.875],"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,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":75,"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.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"leka-ball","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[523.203,418.189,0],"ix":2},"a":{"a":0,"k":[-307.32,95.332,0],"ix":1},"s":{"a":0,"k":[152,152,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[186.625,186.625],"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,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-307.32,95.332],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[87.958,87.958],"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,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[579.665,375.542,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100.956,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[533.465,481.824],"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,0.227166673249,0.469999994016,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[231.772,9.159],"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":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0}]},{"id":"comp_2","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"bouche","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[142.75,-45,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[103.541,95.143,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-3.869,0.442],[0,0]],"o":[[0,0],[3.184,-0.364],[0,0]],"v":[[-88.552,113.599],[-84.432,117.556],[-82.46,111.476]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.6,0.576470588235,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":2,"lj":1,"ml":4,"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":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300.00001221925,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"eye","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[67,41.5,0],"ix":2},"a":{"a":0,"k":[25,25,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":50,"h":50,"ip":0,"op":300.00001221925,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"eye","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[31.5,51,0],"ix":2},"a":{"a":0,"k":[25,25,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":50,"h":50,"ip":0,"op":300.00001221925,"st":0,"bm":0}]},{"id":"comp_3","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25,25,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":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":39,"s":[41,47],"e":[41,10]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":47,"s":[41,10],"e":[41,47]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"n":["0p833_1_0p167_0","0p833_1_0p167_0"],"t":56,"s":[41,47],"e":[41,47]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":213,"s":[41,47],"e":[41,10]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":220,"s":[41,10],"e":[41,47]},{"t":230.000009368092}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":123,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-0.356,0.402],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[48,48],"ix":3},"r":{"a":0,"k":-16.353,"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,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300.00001221925,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":-43,"s":[898.742,862.093,0],"e":[44.864,862.093,0],"to":[-144.891159057617,0,0],"ti":[361.260284423828,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":-21,"s":[44.864,862.093,0],"e":[-964.258,862.093,0],"to":[-412.916168212891,0,0],"ti":[165.608856201172,0,0]},{"t":5.00000020365417}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[60.684,60.684,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.915,0],[0,-8.915],[-8.915,0],[0,8.915]],"o":[[-8.915,0],[0,8.915],[8.915,0],[0,-8.915]],"v":[[0,-16.142],[-16.142,0],[0,16.142],[16.142,0]],"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,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256.884,-262.745],"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,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-43.0000017514259,"op":17.0000006924242,"st":-43.0000017514259,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17,"s":[898.742,862.093,0],"e":[44.864,862.093,0],"to":[-144.891159057617,0,0],"ti":[361.260284423828,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":39,"s":[44.864,862.093,0],"e":[-964.258,862.093,0],"to":[-412.916168212891,0,0],"ti":[165.608856201172,0,0]},{"t":65.0000026475043}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[60.684,60.684,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.915,0],[0,-8.915],[-8.915,0],[0,8.915]],"o":[[-8.915,0],[0,8.915],[8.915,0],[0,-8.915]],"v":[[0,-16.142],[-16.142,0],[0,16.142],[16.142,0]],"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,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256.884,-262.745],"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,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":17.0000006924242,"op":77.0000031362743,"st":17.0000006924242,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":39,"s":[1692.172,200.901,0],"e":[1536.916,200.901,0],"to":[-38.5693702697754,0,0],"ti":[63.7444763183594,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":43,"s":[1536.916,200.901,0],"e":[-170.828,200.901,0],"to":[-449.425933837891,0,0],"ti":[271.930633544922,0,0]},{"t":87.0000035435826}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[60.684,60.684,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.915,0],[0,-8.915],[-8.915,0],[0,8.915]],"o":[[-8.915,0],[0,8.915],[8.915,0],[0,-8.915]],"v":[[0,-16.142],[-16.142,0],[0,16.142],[16.142,0]],"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,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256.884,-262.745],"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,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":39.0000015885026,"op":99.0000040323527,"st":39.0000015885026,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":-21,"s":[1692.172,200.901,0],"e":[1536.916,200.901,0],"to":[-38.5693702697754,0,0],"ti":[63.7444763183594,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":-17,"s":[1536.916,200.901,0],"e":[-170.828,200.901,0],"to":[-449.425933837891,0,0],"ti":[271.930633544922,0,0]},{"t":27.0000010997325}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[60.684,60.684,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.915,0],[0,-8.915],[-8.915,0],[0,8.915]],"o":[[-8.915,0],[0,8.915],[8.915,0],[0,-8.915]],"v":[[0,-16.142],[-16.142,0],[0,16.142],[16.142,0]],"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,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256.884,-262.745],"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,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-21.0000008553475,"op":39.0000015885026,"st":-21.0000008553475,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 8","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":13,"s":[1532.808,724.768,0],"e":[135.558,724.768,0],"to":[-219.556625366211,0,0],"ti":[492.386657714844,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":49,"s":[135.558,724.768,0],"e":[-330.192,724.768,0],"to":[-203.953323364258,0,0],"ti":[90.9433670043945,0,0]},{"t":61.0000024845809}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[60.684,60.684,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.915,0],[0,-8.915],[-8.915,0],[0,8.915]],"o":[[-8.915,0],[0,8.915],[8.915,0],[0,-8.915]],"v":[[0,-16.142],[-16.142,0],[0,16.142],[16.142,0]],"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,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256.884,-262.745],"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,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":13.0000005295009,"op":73.000002973351,"st":13.0000005295009,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":-47,"s":[1532.808,724.768,0],"e":[135.558,724.768,0],"to":[-219.556625366211,0,0],"ti":[492.386657714844,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":-11,"s":[135.558,724.768,0],"e":[-330.192,724.768,0],"to":[-203.953323364258,0,0],"ti":[90.9433670043945,0,0]},{"t":1.00000004073083}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[60.684,60.684,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.915,0],[0,-8.915],[-8.915,0],[0,8.915]],"o":[[-8.915,0],[0,8.915],[8.915,0],[0,-8.915]],"v":[[0,-16.142],[-16.142,0],[0,16.142],[16.142,0]],"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,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256.884,-262.745],"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,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-47.0000019143492,"op":13.0000005295009,"st":-47.0000019143492,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 9","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":37,"s":[1186.954,353.483,0],"e":[294.262,353.483,0],"to":[-150.07438659668,0,0],"ti":[374.935791015625,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":60,"s":[294.262,353.483,0],"e":[-676.046,353.483,0],"to":[-400.796600341797,0,0],"ti":[160.425628662109,0,0]},{"t":85.000003462121}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[75.684,75.684,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.915,0],[0,-8.915],[-8.915,0],[0,8.915]],"o":[[-8.915,0],[0,8.915],[8.915,0],[0,-8.915]],"v":[[0,-16.142],[-16.142,0],[0,16.142],[16.142,0]],"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,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256.884,-262.745],"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,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":37.0000015070409,"op":97.000003950891,"st":37.0000015070409,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Shape Layer 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":-23,"s":[1186.954,353.483,0],"e":[294.262,353.483,0],"to":[-150.07438659668,0,0],"ti":[374.935791015625,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[294.262,353.483,0],"e":[-676.046,353.483,0],"to":[-400.796600341797,0,0],"ti":[160.425628662109,0,0]},{"t":25.0000010182709}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[75.684,75.684,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.915,0],[0,-8.915],[-8.915,0],[0,8.915]],"o":[[-8.915,0],[0,8.915],[8.915,0],[0,-8.915]],"v":[[0,-16.142],[-16.142,0],[0,16.142],[16.142,0]],"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,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256.884,-262.745],"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,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-23.0000009368092,"op":37.0000015070409,"st":-23.0000009368092,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Shape Layer 10","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20,"s":[1186.954,353.483,0],"e":[527.139,353.483,0],"to":[-118.617332458496,0,0],"ti":[286.636993408203,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":37,"s":[527.139,353.483,0],"e":[-676.046,353.483,0],"to":[-463.681579589844,0,0],"ti":[191.882675170898,0,0]},{"t":68.0000027696968}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[75.684,75.684,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.915,0],[0,-8.915],[-8.915,0],[0,8.915]],"o":[[-8.915,0],[0,8.915],[8.915,0],[0,-8.915]],"v":[[0,-16.142],[-16.142,0],[0,16.142],[16.142,0]],"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,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256.884,-262.745],"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,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":20.0000008146167,"op":80.0000032584668,"st":20.0000008146167,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":-40,"s":[1186.954,353.483,0],"e":[527.139,353.483,0],"to":[-118.617332458496,0,0],"ti":[286.636993408203,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":-23,"s":[527.139,353.483,0],"e":[-676.046,353.483,0],"to":[-463.681579589844,0,0],"ti":[191.882675170898,0,0]},{"t":8.00000032584668}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[75.684,75.684,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.915,0],[0,-8.915],[-8.915,0],[0,8.915]],"o":[[-8.915,0],[0,8.915],[8.915,0],[0,-8.915]],"v":[[0,-16.142],[-16.142,0],[0,16.142],[16.142,0]],"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,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256.884,-262.745],"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,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-40.0000016292334,"op":20.0000008146167,"st":-40.0000016292334,"bm":0},{"ddd":0,"ind":11,"ty":0,"nm":"stars","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":23,"s":[1126,696,0],"e":[-86,696,0],"to":[-202,0,0],"ti":[202,0,0]},{"t":60.0000024438501}],"ix":2},"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":200,"h":200,"ip":23.0000009368092,"op":77.0000031362743,"st":17.0000006924242,"bm":0},{"ddd":0,"ind":12,"ty":0,"nm":"stars","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":-22,"s":[1126,568.848,0],"e":[405.351,568.848,0],"to":[-116.357513427734,0,0],"ti":[286.8583984375,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[405.351,568.848,0],"e":[-86,568.848,0],"to":[-211.13606262207,0,0],"ti":[85.6424865722656,0,0]},{"t":15.0000006109625}],"ix":2},"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":200,"h":200,"ip":-22.0000008960784,"op":32.0000013033867,"st":-28.0000011404634,"bm":0},{"ddd":0,"ind":13,"ty":0,"nm":"stars","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":38,"s":[1126,568.848,0],"e":[405.351,568.848,0],"to":[-116.357513427734,0,0],"ti":[286.8583984375,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":60,"s":[405.351,568.848,0],"e":[-86,568.848,0],"to":[-211.13606262207,0,0],"ti":[85.6424865722656,0,0]},{"t":75.0000030548126}],"ix":2},"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":200,"h":200,"ip":38.0000015477717,"op":92.0000037472368,"st":32.0000013033867,"bm":0},{"ddd":0,"ind":14,"ty":0,"nm":"stars","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[1126,568.848,0],"e":[-86,568.848,0],"to":[-202,0,0],"ti":[202,0,0]},{"t":37.0000015070409}],"ix":2},"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":200,"h":200,"ip":0,"op":54.0000021994651,"st":-6.00000024438501,"bm":0},{"ddd":0,"ind":15,"ty":0,"nm":"stars","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":17,"s":[1126,77.192,0],"e":[-86,77.192,0],"to":[-202,0,0],"ti":[202,0,0]},{"t":54.0000021994651}],"ix":2},"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":200,"h":200,"ip":17.0000006924242,"op":71.0000028918893,"st":11.0000004480392,"bm":0},{"ddd":0,"ind":16,"ty":0,"nm":"stars","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[1126,77.192,0],"e":[-86,77.192,0],"to":[-202,0,0],"ti":[202,0,0]},{"t":37.0000015070409}],"ix":2},"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":200,"h":200,"ip":0,"op":54.0000021994651,"st":-6.00000024438501,"bm":0},{"ddd":0,"ind":17,"ty":0,"nm":"stars","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":27,"s":[1126,77.192,0],"e":[45.03,77.192,0],"to":[-67.3434982299805,0,0],"ti":[299.780029296875,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":60,"s":[45.03,77.192,0],"e":[-86,77.192,0],"to":[-76.7516479492188,0,0],"ti":[4.66899824142456,0,0]},{"t":64.0000026067734}],"ix":2},"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":200,"h":200,"ip":27.0000010997325,"op":81.0000032991976,"st":21.0000008553475,"bm":0},{"ddd":0,"ind":18,"ty":0,"nm":"stars","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":-33,"s":[1126,77.192,0],"e":[45.03,77.192,0],"to":[-67.3434982299805,0,0],"ti":[299.780029296875,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[45.03,77.192,0],"e":[-86,77.192,0],"to":[-76.7516479492188,0,0],"ti":[4.66899824142456,0,0]},{"t":4.00000016292334}],"ix":2},"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":200,"h":200,"ip":-33.0000013441176,"op":21.0000008553475,"st":-39.0000015885026,"bm":0},{"ddd":0,"ind":19,"ty":0,"nm":"stars","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":40,"s":[1126,308.98,0],"e":[470.868,308.98,0],"to":[-56.6350288391113,0,0],"ti":[291.645416259766,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":60,"s":[470.868,308.98,0],"e":[-86,308.98,0],"to":[-262.223815917969,0,0],"ti":[44.7132606506348,0,0]},{"t":77.0000031362743}],"ix":2},"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":200,"h":200,"ip":40.0000016292334,"op":94.0000038286985,"st":34.0000013848484,"bm":0},{"ddd":0,"ind":20,"ty":0,"nm":"stars","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":-20,"s":[1126,308.98,0],"e":[470.868,308.98,0],"to":[-56.6350288391113,0,0],"ti":[291.645416259766,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[470.868,308.98,0],"e":[-86,308.98,0],"to":[-262.223815917969,0,0],"ti":[44.7132606506348,0,0]},{"t":17.0000006924242}],"ix":2},"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":200,"h":200,"ip":-20.0000008146167,"op":34.0000013848484,"st":-26.0000010590017,"bm":0},{"ddd":0,"ind":21,"ty":0,"nm":"stars","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":9,"s":[1126,308.98,0],"e":[-86,308.98,0],"to":[-202,0,0],"ti":[202,0,0]},{"t":46.0000018736184}],"ix":2},"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":200,"h":200,"ip":9.00000036657752,"op":63.0000025660426,"st":3.00000012219251,"bm":0},{"ddd":0,"ind":22,"ty":0,"nm":"rainbowleka","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[512,418,0],"e":[512,379,0],"to":[0,-6.5,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[512,379,0],"e":[512,418,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20,"s":[512,418,0],"e":[512,379,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":30,"s":[512,379,0],"e":[512,418,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":40,"s":[512,418,0],"e":[512,379,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":50,"s":[512,379,0],"e":[512,418,0],"to":[0,0,0],"ti":[0,-6.5,0]},{"t":60.0000024438501}],"ix":2},"a":{"a":0,"k":[512,384,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1024,"h":768,"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":"violet","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[758.822,267.152,0],"e":[-196.242,270.152,0],"to":[-155.243835449219,0,0],"ti":[205.495452880859,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":58,"s":[-196.242,270.152,0],"e":[-229.178,267.152,0],"to":[-12.4729557037354,0,0],"ti":[9.42283344268799,0,0]},{"t":60.0000024438501}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[186.05,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-35.313,-0.401],[-35.689,0],[-36.244,0],[-35.686,0.406],[-36.07,0],[-35.522,-0.413],[-35.135,-0.399],[-36.618,-0.411],[-35.689,0],[-36.235,0.824],[0,0]],"o":[[0,0],[35.686,0.406],[36.244,0],[35.689,0],[36.068,-0.41],[35.525,0],[35.135,0.409],[36.618,0.416],[35.686,0.401],[36.244,0],[35.911,-0.816],[0,0]],"v":[[-495.916,146.764],[-407.456,88.461],[-318.995,148.775],[-228.524,88.461],[-140.063,148.775],[-51.602,86.45],[34.848,148.775],[121.298,88.461],[211.77,150.785],[300.23,90.471],[390.702,150.785],[477.152,86.45]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.564705882353,0.074509803922,0.996078431373,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":26,"ix":5},"lc":1,"lj":1,"ml":4,"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":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":24,"ty":4,"nm":"vert","parent":23,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,28,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],[-35.313,-0.401],[-35.689,0],[-36.244,0],[-35.686,0.406],[-36.07,0],[-35.522,-0.413],[-35.135,-0.399],[-36.618,-0.411],[-35.689,0],[-36.235,0.824],[0,0]],"o":[[0,0],[35.686,0.406],[36.244,0],[35.689,0],[36.068,-0.41],[35.525,0],[35.135,0.409],[36.618,0.416],[35.686,0.401],[36.244,0],[35.911,-0.816],[0,0]],"v":[[-495.916,146.764],[-407.456,88.461],[-318.995,148.775],[-228.524,88.461],[-140.063,148.775],[-51.602,86.45],[34.848,148.775],[121.298,88.461],[211.77,150.785],[300.23,90.471],[390.702,150.785],[477.152,86.45]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.313725490196,0.890196078431,0.760784313725,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":30,"ix":5},"lc":1,"lj":1,"ml":4,"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":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":25,"ty":4,"nm":"jaune","parent":23,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,57,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],[-35.313,-0.401],[-35.689,0],[-36.244,0],[-35.686,0.406],[-36.07,0],[-35.522,-0.413],[-35.135,-0.399],[-36.618,-0.411],[-35.689,0],[-36.235,0.824],[0,0]],"o":[[0,0],[35.686,0.406],[36.244,0],[35.689,0],[36.068,-0.41],[35.525,0],[35.135,0.409],[36.618,0.416],[35.686,0.401],[36.244,0],[35.911,-0.816],[0,0]],"v":[[-495.916,146.764],[-407.456,88.461],[-318.995,148.775],[-228.524,88.461],[-140.063,148.775],[-51.602,86.45],[34.848,148.775],[121.298,88.461],[211.77,150.785],[300.23,90.471],[390.702,150.785],[477.152,86.45]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.972549019608,0.905882352941,0.109803921569,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":30,"ix":5},"lc":1,"lj":1,"ml":4,"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":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":26,"ty":4,"nm":"rouge","parent":23,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,86,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],[-35.313,-0.401],[-35.689,0],[-36.244,0],[-35.686,0.406],[-36.07,0],[-35.522,-0.413],[-35.135,-0.399],[-36.618,-0.411],[-35.689,0],[-36.235,0.824],[0,0]],"o":[[0,0],[35.686,0.406],[36.244,0],[35.689,0],[36.068,-0.41],[35.525,0],[35.135,0.409],[36.618,0.416],[35.686,0.401],[36.244,0],[35.911,-0.816],[0,0]],"v":[[-495.916,146.764],[-407.456,88.461],[-318.995,148.775],[-228.524,88.461],[-140.063,148.775],[-51.602,86.45],[34.848,148.775],[121.298,88.461],[211.77,150.785],[300.23,90.471],[390.702,150.785],[477.152,86.45]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.23137254902,0.188235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":30,"ix":5},"lc":1,"lj":1,"ml":4,"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":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":27,"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":[512,384,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":[1039.535,780.879],"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,0.227450995352,0.470587995941,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[1.768,0.439],"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":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Modules/GameEngineKit/Resources/animations/dance_freeze_freeze.animation.lottie.json b/Modules/GameEngineKit/Resources/animations/dance_freeze_freeze.animation.lottie.json new file mode 100644 index 0000000000..9daeb452e7 --- /dev/null +++ b/Modules/GameEngineKit/Resources/animations/dance_freeze_freeze.animation.lottie.json @@ -0,0 +1 @@ +{"v":"5.1.1","fr":29.9700012207031,"ip":8.00000032584668,"op":60.0000024438501,"w":1024,"h":768,"nm":"freeze","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":300,"ix":10},"p":{"a":0,"k":[103.451,98.076,0],"ix":2},"a":{"a":0,"k":[4.049,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[0,0,100],"e":[20,20,100]},{"t":21.0000008553475}],"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":[[3.599,-77.61],[3.599,-200.436]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[99]},{"t":31.0000012626559}],"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":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":32,"ix":5},"lc":2,"lj":2,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":240,"ix":10},"p":{"a":0,"k":[103.451,98.076,0],"ix":2},"a":{"a":0,"k":[4.049,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[0,0,100],"e":[20,20,100]},{"t":21.0000008553475}],"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":[[3.599,-77.61],[3.599,-200.436]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[99]},{"t":31.0000012626559}],"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":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":32,"ix":5},"lc":2,"lj":2,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":0,"k":[103.451,98.076,0],"ix":2},"a":{"a":0,"k":[4.049,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[0,0,100],"e":[20,20,100]},{"t":21.0000008553475}],"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":[[3.599,-77.61],[3.599,-200.436]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[99]},{"t":31.0000012626559}],"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":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":32,"ix":5},"lc":2,"lj":2,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":120,"ix":10},"p":{"a":0,"k":[103.451,98.076,0],"ix":2},"a":{"a":0,"k":[4.049,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[0,0,100],"e":[20,20,100]},{"t":21.0000008553475}],"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":[[3.599,-77.61],[3.599,-200.436]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[99]},{"t":31.0000012626559}],"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":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":32,"ix":5},"lc":2,"lj":2,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":60,"ix":10},"p":{"a":0,"k":[103.451,98.076,0],"ix":2},"a":{"a":0,"k":[4.049,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[0,0,100],"e":[20,20,100]},{"t":21.0000008553475}],"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":[[3.599,-77.61],[3.599,-200.436]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[99]},{"t":31.0000012626559}],"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":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":32,"ix":5},"lc":2,"lj":2,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[103.451,98.076,0],"ix":2},"a":{"a":0,"k":[4.049,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[0,0,100],"e":[20,20,100]},{"t":21.0000008553475}],"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":[[3.599,-77.61],[3.599,-200.436]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[99]},{"t":31.0000012626559}],"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":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":32,"ix":5},"lc":2,"lj":2,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":0,"nm":"face","parent":4,"refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-6.012,"ix":10},"p":{"a":0,"k":[-307.32,95.332,0],"ix":2},"a":{"a":0,"k":[14.68,60.332,0],"ix":1},"s":{"a":0,"k":[91,91,100],"ix":6}},"ao":0,"w":100,"h":100,"ip":0,"op":300.00001221925,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"reflets 2","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":907.776,"ix":10},"p":{"a":0,"k":[-306.683,97.77,0],"ix":2},"a":{"a":0,"k":[-306.562,97.875,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":[[39.025,0],[0,-39.025],[-39.025,0],[0,39.025]],"o":[[-39.025,0],[0,39.025],[39.025,0],[0,-39.025]],"v":[[0,-70.66],[-70.66,0],[0,70.66],[70.66,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.901960784314,0.901960784314,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-306.562,97.875],"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,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":75,"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.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"reflets","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":853.315,"ix":10},"p":{"a":0,"k":[-307.751,95.884,0],"ix":2},"a":{"a":0,"k":[-306.562,97.875,0],"ix":1},"s":{"a":0,"k":[103.784,103.784,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[39.025,0],[0,-39.025],[-39.025,0],[0,39.025]],"o":[[-39.025,0],[0,39.025],[39.025,0],[0,-39.025]],"v":[[0,-70.66],[-70.66,0],[0,70.66],[70.66,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.901960784314,0.901960784314,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-306.562,97.875],"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,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":75,"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.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"leka-ball","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[523.203,418.189,0],"ix":2},"a":{"a":0,"k":[-307.32,95.332,0],"ix":1},"s":{"a":0,"k":[152,152,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[186.625,186.625],"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,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-307.32,95.332],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[87.958,87.958],"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,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0}]},{"id":"comp_2","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"bouche","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[142.75,-45,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[103.541,95.143,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-3.869,0.442],[0,0]],"o":[[0,0],[3.184,-0.364],[0,0]],"v":[[-88.552,113.599],[-84.432,117.556],[-82.46,111.476]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.6,0.576470588235,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":2,"lj":1,"ml":4,"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":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300.00001221925,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"eye","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[67,41.5,0],"ix":2},"a":{"a":0,"k":[25,25,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":50,"h":50,"ip":0,"op":300.00001221925,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"eye","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[31.5,51,0],"ix":2},"a":{"a":0,"k":[25,25,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":50,"h":50,"ip":0,"op":300.00001221925,"st":0,"bm":0}]},{"id":"comp_3","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[25,25,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":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":39,"s":[41,47],"e":[41,10]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":47,"s":[41,10],"e":[41,47]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"n":["0p833_1_0p167_0","0p833_1_0p167_0"],"t":56,"s":[41,47],"e":[41,47]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":213,"s":[41,47],"e":[41,10]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":220,"s":[41,10],"e":[41,47]},{"t":230.000009368092}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":123,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-0.356,0.402],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[48,48],"ix":3},"r":{"a":0,"k":-16.353,"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,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300.00001221925,"st":0,"bm":0}]},{"id":"comp_4","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"reflet4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":119,"ix":10},"p":{"a":0,"k":[519.515,245.66,0],"ix":2},"a":{"a":0,"k":[-151.5,113.25,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]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":2,"s":[0,0,100],"e":[63.985,85.377,100]},{"t":6.00000024438501}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7,1.5],[-3.824,-9.101],[-3.016,8.712],[4.243,6.718]],"o":[[-7.577,-1.624],[1.989,4.733],[2.25,-6.5],[-6,-9.5]],"v":[[-158.75,79],[-161.426,119.101],[-131.25,132.75],[-151.5,113.25]],"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,"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":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"reflet3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":243,"ix":10},"p":{"a":0,"k":[664.515,495.66,0],"ix":2},"a":{"a":0,"k":[-151.5,113.25,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]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":2,"s":[0,0,100],"e":[100,100,100]},{"t":6.00000024438501}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7,1.5],[-3.824,-9.101],[-3.016,8.712],[4.243,6.718]],"o":[[-7.577,-1.624],[1.989,4.733],[2.25,-6.5],[-6,-9.5]],"v":[[-158.75,79],[-161.426,119.101],[-131.25,132.75],[-151.5,113.25]],"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,"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":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"reflet2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[512,384,0],"ix":2},"a":{"a":0,"k":[0,0,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]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":2,"s":[0,0,100],"e":[100,100,100]},{"t":6.00000024438501}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7,1.5],[-3.824,-9.101],[-3.016,8.712],[4.243,6.718]],"o":[[-7.577,-1.624],[1.989,4.733],[2.25,-6.5],[-6,-9.5]],"v":[[-158.75,79],[-161.426,119.101],[-131.25,132.75],[-151.5,113.25]],"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,"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":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"reflet","sr":1,"ks":{"o":{"a":0,"k":90,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[512,384,0],"ix":2},"a":{"a":0,"k":[0,0,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]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":2,"s":[0,0,100],"e":[100,100,100]},{"t":6.00000024438501}],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Twirl","np":5,"mn":"ADBE Twirl","ix":1,"en":1,"ef":[{"ty":0,"nm":"Angle","mn":"ADBE Twirl-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":0,"nm":"Twirl Radius","mn":"ADBE Twirl-0002","ix":2,"v":{"a":0,"k":30,"ix":2}},{"ty":3,"nm":"Twirl Center","mn":"ADBE Twirl-0003","ix":3,"v":{"a":0,"k":[512,384],"ix":3}}]},{"ty":5,"nm":"Liquify","np":19,"mn":"ADBE LIQUIFY","ix":2,"en":1,"ef":[{"ty":6,"nm":"Tools","mn":"ADBE LIQUIFY-0001","ix":1,"v":0},{"ty":6,"nm":"Tool Options","mn":"ADBE LIQUIFY-0002","ix":2,"v":0},{"ty":0,"nm":"Brush Size","mn":"ADBE LIQUIFY-0003","ix":3,"v":{"a":0,"k":64,"ix":3}},{"ty":0,"nm":"Brush Pressure","mn":"ADBE LIQUIFY-0004","ix":4,"v":{"a":0,"k":50,"ix":4}},{"ty":10,"nm":"Freeze Area Mask","mn":"ADBE LIQUIFY-0007","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Turbulent Jitter","mn":"ADBE LIQUIFY-0005","ix":6,"v":{"a":0,"k":70,"ix":6}},{"ty":7,"nm":"Clone Offset","mn":"ADBE LIQUIFY-0017","ix":7,"v":{"a":0,"k":0,"ix":7}},{"ty":7,"nm":"Reconstruction Mode","mn":"ADBE LIQUIFY-0006","ix":8,"v":{"a":0,"k":1,"ix":8}},{"ty":6,"nm":"Tool Options","mn":"ADBE LIQUIFY-0008","ix":9,"v":0},{"ty":6,"nm":"View Options","mn":"ADBE LIQUIFY-0009","ix":10,"v":0},{"ty":7,"nm":"View Mesh","mn":"ADBE LIQUIFY-0010","ix":11,"v":{"a":0,"k":0,"ix":11}},{"ty":7,"nm":"Mesh Size","mn":"ADBE LIQUIFY-0011","ix":12,"v":{"a":0,"k":2,"ix":12}},{"ty":7,"nm":"Mesh Color","mn":"ADBE LIQUIFY-0012","ix":13,"v":{"a":0,"k":7,"ix":13}},{"ty":6,"nm":"View Options","mn":"ADBE LIQUIFY-0013","ix":14,"v":0},{},{"ty":3,"nm":"Distortion Mesh Offset","mn":"ADBE LIQUIFY-0015","ix":16,"v":{"a":0,"k":[0,0],"ix":16}},{"ty":0,"nm":"Distortion Percentage","mn":"ADBE LIQUIFY-0016","ix":17,"v":{"a":0,"k":100,"ix":17}}]}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[37.313,0.772],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[-41.453,-0.858],[0,0],[0,0],[0,0],[0,0]],"v":[[143.403,-56.306],[2.419,-7.21],[-136.113,-56.952],[-4.532,4.339],[-2.581,191.984],[7.516,4.839]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":31,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false},{"ty":"st","c":{"a":0,"k":[0.858823529412,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":2,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.860263480392,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"icecube","parent":3,"sr":1,"ks":{"o":{"a":0,"k":63,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,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]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"t":4.00000016292334}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[0.617,1.906],"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":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[912.309,340.953],"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":"Rectangle 2","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[1.422,2.648],"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":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[892.711,223.324],"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":"Rectangle 1","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"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":"Shape 2","np":2,"cix":2,"ix":3,"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],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[3.879,-161.616],[-178.424,-89.212],[-178.424,130.586],[2.586,227.556],[178.424,126.707],[178.424,-87.919]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":20,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"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":"Shape 1","np":4,"cix":2,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"leka-frozen","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[513,369.375,0],"ix":2},"a":{"a":0,"k":[512,384,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Liquify","np":19,"mn":"ADBE LIQUIFY","ix":1,"en":1,"ef":[{"ty":6,"nm":"Tools","mn":"ADBE LIQUIFY-0001","ix":1,"v":0},{"ty":6,"nm":"Tool Options","mn":"ADBE LIQUIFY-0002","ix":2,"v":0},{"ty":0,"nm":"Brush Size","mn":"ADBE LIQUIFY-0003","ix":3,"v":{"a":0,"k":64,"ix":3}},{"ty":0,"nm":"Brush Pressure","mn":"ADBE LIQUIFY-0004","ix":4,"v":{"a":0,"k":50,"ix":4}},{"ty":10,"nm":"Freeze Area Mask","mn":"ADBE LIQUIFY-0007","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Turbulent Jitter","mn":"ADBE LIQUIFY-0005","ix":6,"v":{"a":0,"k":70,"ix":6}},{"ty":7,"nm":"Clone Offset","mn":"ADBE LIQUIFY-0017","ix":7,"v":{"a":0,"k":0,"ix":7}},{"ty":7,"nm":"Reconstruction Mode","mn":"ADBE LIQUIFY-0006","ix":8,"v":{"a":0,"k":1,"ix":8}},{"ty":6,"nm":"Tool Options","mn":"ADBE LIQUIFY-0008","ix":9,"v":0},{"ty":6,"nm":"View Options","mn":"ADBE LIQUIFY-0009","ix":10,"v":0},{"ty":7,"nm":"View Mesh","mn":"ADBE LIQUIFY-0010","ix":11,"v":{"a":0,"k":0,"ix":11}},{"ty":7,"nm":"Mesh Size","mn":"ADBE LIQUIFY-0011","ix":12,"v":{"a":0,"k":2,"ix":12}},{"ty":7,"nm":"Mesh Color","mn":"ADBE LIQUIFY-0012","ix":13,"v":{"a":0,"k":7,"ix":13}},{"ty":6,"nm":"View Options","mn":"ADBE LIQUIFY-0013","ix":14,"v":0},{},{"ty":3,"nm":"Distortion Mesh Offset","mn":"ADBE LIQUIFY-0015","ix":16,"v":{"a":0,"k":[0,0],"ix":16}},{"ty":0,"nm":"Distortion Percentage","mn":"ADBE LIQUIFY-0016","ix":17,"v":{"a":0,"k":100,"ix":17}}]},{"ty":5,"nm":"Gaussian Blur","np":6,"mn":"ADBE Gaussian Blur 2","ix":2,"en":1,"ef":[{"ty":0,"nm":"Blurriness","mn":"ADBE Gaussian Blur 2-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"Blur Dimensions","mn":"ADBE Gaussian Blur 2-0002","ix":2,"v":{"a":0,"k":1,"ix":2}},{"ty":7,"nm":"Repeat Edge Pixels","mn":"ADBE Gaussian Blur 2-0003","ix":3,"v":{"a":0,"k":0,"ix":3}},{"ty":7,"nm":"GPU Rendering","mn":"ADBE Force CPU GPU","ix":4,"v":{"a":0,"k":1,"ix":4}}]}],"w":1024,"h":768,"ip":4.00000016292334,"op":64.0000026067734,"st":4.00000016292334,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"stalagmites 2","sr":1,"ks":{"o":{"a":0,"k":75,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":4,"s":[512,691,0],"e":[512,442,0],"to":[0,-41.5,0],"ti":[0,41.5,0]},{"t":8.00000032584668}],"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":[[-2,-6],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[2,6],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-202,-532],[-520,-418],[-438,-174],[-390,-426],[-386,-212],[-356,-432],[-298,-220],[-264,-434],[-220,-304],[-180,-402],[-140,-280],[-90,-410],[-82,-184],[-32,-408],[10,-308],[40,-424],[54,-258],[86,-420],[124,-234],[144,-400],[180,-186],[208,-428],[240,-104],[276,-426],[304,-292],[338,-414],[368,-238],[416,-422],[436,-232],[470,-430],[496,-186],[554,-426],[390,-528]],"c":true},"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":10,"ix":5},"lc":2,"lj":2,"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,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-3.516,31.398],"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":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"stalagmites","sr":1,"ks":{"o":{"a":0,"k":75,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":4,"s":[516,64,0],"e":[516,332,0],"to":[0,44.6666679382324,0],"ti":[0,-44.6666679382324,0]},{"t":8.00000032584668}],"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":[[-2,-6],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[2,6],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-202,-532],[-520,-418],[-438,-174],[-390,-426],[-386,-212],[-356,-432],[-298,-220],[-264,-434],[-220,-304],[-180,-402],[-140,-280],[-90,-410],[-82,-184],[-32,-408],[10,-308],[40,-424],[54,-258],[86,-420],[124,-234],[144,-400],[180,-186],[208,-428],[240,-104],[276,-426],[304,-292],[338,-414],[368,-238],[416,-422],[436,-232],[470,-430],[496,-186],[554,-426],[390,-528]],"c":true},"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":10,"ix":5},"lc":2,"lj":2,"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,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-3.516,31.398],"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":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":41,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"stars","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[45.03,77.192,0],"ix":2},"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":200,"h":200,"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[294.262,353.483,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[75.684,75.684,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.915,0],[0,-8.915],[-8.915,0],[0,8.915]],"o":[[-8.915,0],[0,8.915],[8.915,0],[0,-8.915]],"v":[[0,-16.142],[-16.142,0],[0,16.142],[16.142,0]],"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,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256.884,-262.745],"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,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"stars","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[405.351,568.848,0],"ix":2},"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":200,"h":200,"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"leka-frozen","refId":"comp_1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[100],"e":[0]},{"t":6.00000024438501}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[512,384,0],"ix":2},"a":{"a":0,"k":[512,384,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1024,"h":768,"ip":0,"op":6.00000024438501,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"leka-ice-cube","refId":"comp_4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":12,"s":[529,384,0],"e":[554,384,0],"to":[4.16666650772095,0,0],"ti":[5.33333349227905,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":14.071,"s":[554,384,0],"e":[497,384,0],"to":[-5.33333349227905,0,0],"ti":[-0.33333334326744,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":15.988,"s":[497,384,0],"e":[556,384,0],"to":[0.33333334326744,0,0],"ti":[0.33333334326744,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":17.655,"s":[556,384,0],"e":[495,384,0],"to":[-0.33333334326744,0,0],"ti":[4.5,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":19.59,"s":[495,384,0],"e":[529,384,0],"to":[-4.5,0,0],"ti":[-5.66666650772095,0,0]},{"t":22.0000008960784}],"ix":2},"a":{"a":0,"k":[512,384,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1024,"h":768,"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"background 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":4,"s":[100],"e":[0]},{"t":8.00000032584668}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[512,384,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":[1027.485,775.975],"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,0.227450980392,0.470588235294,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-2.022,0.223],"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":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":9,"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":[512,384,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":[1025.642,770.156],"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.709543504902,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-2.139,-3.361],"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":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Modules/GameEngineKit/Resources/animations/hide_and_seek_hidden.animation.lottie.json b/Modules/GameEngineKit/Resources/animations/hide_and_seek_hidden.animation.lottie.json new file mode 100644 index 0000000000..17b23720c4 --- /dev/null +++ b/Modules/GameEngineKit/Resources/animations/hide_and_seek_hidden.animation.lottie.json @@ -0,0 +1 @@ +{"v":"5.1.1","fr":29.9700012207031,"ip":0,"op":90.0000036657751,"w":1024,"h":768,"nm":"hide-and-seek","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[600,384,0],"ix":2},"a":{"a":0,"k":[-91.062,-3.612,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":13,"s":[57.902,57.902],"e":[57.902,8]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":18,"s":[57.902,8],"e":[57.902,57.902]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":23,"s":[57.902,57.902],"e":[57.902,57.902]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":37,"s":[57.902,57.902],"e":[57.902,8]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":42,"s":[57.902,8],"e":[57.902,57.902]},{"t":47.0000019143492}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":81,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-91.062,-3.612],"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":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90.0000036657751,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[422,384,0],"ix":2},"a":{"a":0,"k":[-91.062,-3.612,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":13,"s":[57.902,57.902],"e":[57.902,8]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":18,"s":[57.902,8],"e":[57.902,57.902]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":23,"s":[57.902,57.902],"e":[57.902,57.902]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":37,"s":[57.902,57.902],"e":[57.902,8]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":42,"s":[57.902,8],"e":[57.902,57.902]},{"t":47.0000019143492}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":81,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-91.062,-3.612],"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":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90.0000036657751,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Modules/GameEngineKit/Resources/animations/reinforcer_spin_blink.animation.lottie.json b/Modules/GameEngineKit/Resources/animations/reinforcer_spin_blink.animation.lottie.json new file mode 100644 index 0000000000..9ca5179bc7 --- /dev/null +++ b/Modules/GameEngineKit/Resources/animations/reinforcer_spin_blink.animation.lottie.json @@ -0,0 +1 @@ +{"v":"4.12.0","fr":29.9700012207031,"ip":0,"op":37.0000015070409,"w":1024,"h":768,"nm":"motivator","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":217,"ix":10},"p":{"a":0,"k":[325,324.5,0],"ix":2},"a":{"a":0,"k":[7,-45,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],[87.784,99.411],[-79.529,70.227],[-56.182,-63.623],[50.898,-44.945],[35.956,40.719],[-32.575,28.765],[-23.012,-26.06],[20.848,-18.41],[14.728,16.678],[-13.343,11.782]],"o":[[-99.411,87.784],[-70.227,-79.529],[63.623,-56.182],[44.945,50.899],[-40.719,35.956],[-28.765,-32.575],[26.06,-23.012],[18.41,20.848],[-16.678,14.728],[-11.782,-13.343],[0,0]],"v":[[204.587,130.304],[-134.36,109.251],[-117.518,-161.906],[99.408,-148.433],[88.63,25.108],[-50.203,16.485],[-43.305,-94.581],[45.548,-89.062],[41.133,-17.98],[-15.733,-21.512],[-12.907,-67.005]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.481],"y":[1]},"o":{"x":[0.205],"y":[0.527]},"n":["0p481_1_0p205_0p527"],"t":7.193,"s":[45.333],"e":[0]},{"t":17.9820007324219}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.397],"y":[1]},"o":{"x":[0.203],"y":[0.398]},"n":["0p397_1_0p203_0p398"],"t":7.193,"s":[76.02],"e":[0]},{"t":17.9820007324219}],"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.23137254902,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":7.193,"s":[15],"e":[5]},{"t":17.9820007324219}],"ix":5},"lc":2,"lj":1,"ml":4,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":8.39160034179688,"op":33.5664013671875,"st":-1.19880004882812,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":87,"ix":10},"p":{"a":0,"k":[325,324.5,0],"ix":2},"a":{"a":0,"k":[7,-45,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],[87.784,99.411],[-79.529,70.227],[-56.182,-63.623],[50.898,-44.945],[35.956,40.719],[-32.575,28.765],[-23.012,-26.06],[20.848,-18.41],[14.728,16.678],[-13.343,11.782]],"o":[[-99.411,87.784],[-70.227,-79.529],[63.623,-56.182],[44.945,50.899],[-40.719,35.956],[-28.765,-32.575],[26.06,-23.012],[18.41,20.848],[-16.678,14.728],[-11.782,-13.343],[0,0]],"v":[[204.587,130.304],[-134.36,109.251],[-117.518,-161.906],[99.408,-148.433],[88.63,25.108],[-50.203,16.485],[-43.305,-94.581],[45.548,-89.062],[41.133,-17.98],[-15.733,-21.512],[-12.907,-67.005]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.481],"y":[1]},"o":{"x":[0.205],"y":[1.054]},"n":["0p481_1_0p205_1p054"],"t":8.392,"s":[45.333],"e":[0]},{"t":29.9700012207031}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.397],"y":[1]},"o":{"x":[0.203],"y":[0.796]},"n":["0p397_1_0p203_0p796"],"t":8.392,"s":[76.02],"e":[0]},{"t":29.9700012207031}],"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.626256127451,0.250536137001,0.797028186275,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":8.392,"s":[15],"e":[5]},{"t":29.9700012207031}],"ix":5},"lc":2,"lj":1,"ml":4,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":8.39160034179688,"op":33.5664013671875,"st":-1.19880004882812,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-33,"ix":10},"p":{"a":0,"k":[325,324.5,0],"ix":2},"a":{"a":0,"k":[7,-45,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],[87.784,99.411],[-79.529,70.227],[-56.182,-63.623],[50.898,-44.945],[35.956,40.719],[-32.575,28.765],[-23.012,-26.06],[20.848,-18.41],[14.728,16.678],[-13.343,11.782]],"o":[[-99.411,87.784],[-70.227,-79.529],[63.623,-56.182],[44.945,50.899],[-40.719,35.956],[-28.765,-32.575],[26.06,-23.012],[18.41,20.848],[-16.678,14.728],[-11.782,-13.343],[0,0]],"v":[[204.587,130.304],[-134.36,109.251],[-117.518,-161.906],[99.408,-148.433],[88.63,25.108],[-50.203,16.485],[-43.305,-94.581],[45.548,-89.062],[41.133,-17.98],[-15.733,-21.512],[-12.907,-67.005]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.481],"y":[1]},"o":{"x":[0.205],"y":[1.112]},"n":["0p481_1_0p205_1p112"],"t":7.193,"s":[45.333],"e":[0]},{"t":29.9700012207031}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.397],"y":[1]},"o":{"x":[0.203],"y":[0.84]},"n":["0p397_1_0p203_0p84"],"t":7.193,"s":[76.02],"e":[0]},{"t":29.9700012207031}],"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.23137254902,1,0.988051470588,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":7.193,"s":[15],"e":[5]},{"t":29.9700012207031}],"ix":5},"lc":2,"lj":1,"ml":4,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":7.19280029296875,"op":32.3676013183594,"st":-2.39760009765625,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":217,"ix":10},"p":{"a":0,"k":[325,324.5,0],"ix":2},"a":{"a":0,"k":[7,-45,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]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[24,24,100],"e":[100,100,100]},{"t":13.1868005371094}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[87.784,99.411],[-79.529,70.227],[-56.182,-63.623],[50.898,-44.945],[35.956,40.719],[-32.575,28.765],[-23.012,-26.06],[20.848,-18.41],[14.728,16.678],[-13.343,11.782]],"o":[[-99.411,87.784],[-70.227,-79.529],[63.623,-56.182],[44.945,50.899],[-40.719,35.956],[-28.765,-32.575],[26.06,-23.012],[18.41,20.848],[-16.678,14.728],[-11.782,-13.343],[0,0]],"v":[[204.587,130.304],[-134.36,109.251],[-117.518,-161.906],[99.408,-148.433],[88.63,25.108],[-50.203,16.485],[-43.305,-94.581],[45.548,-89.062],[41.133,-17.98],[-15.733,-21.512],[-12.907,-67.005]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.481],"y":[1]},"o":{"x":[0.205],"y":[0.937]},"n":["0p481_1_0p205_0p937"],"t":1.199,"s":[45.333],"e":[0]},{"t":20.3796008300781}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.397],"y":[1]},"o":{"x":[0.203],"y":[0.708]},"n":["0p397_1_0p203_0p708"],"t":1.199,"s":[76.02],"e":[0]},{"t":20.3796008300781}],"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.476087622549,0.243979779412,0.762515318627,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":1.199,"s":[15],"e":[5]},{"t":20.3796008300781}],"ix":5},"lc":2,"lj":1,"ml":4,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1.19880004882812,"op":31.1688012695313,"st":-8.39160034179688,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":87,"ix":10},"p":{"a":0,"k":[325,324.5,0],"ix":2},"a":{"a":0,"k":[7,-45,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]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[24,24,100],"e":[100,100,100]},{"t":13.1868005371094}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[87.784,99.411],[-79.529,70.227],[-56.182,-63.623],[50.898,-44.945],[35.956,40.719],[-32.575,28.765],[-23.012,-26.06],[20.848,-18.41],[14.728,16.678],[-13.343,11.782]],"o":[[-99.411,87.784],[-70.227,-79.529],[63.623,-56.182],[44.945,50.899],[-40.719,35.956],[-28.765,-32.575],[26.06,-23.012],[18.41,20.848],[-16.678,14.728],[-11.782,-13.343],[0,0]],"v":[[204.587,130.304],[-134.36,109.251],[-117.518,-161.906],[99.408,-148.433],[88.63,25.108],[-50.203,16.485],[-43.305,-94.581],[45.548,-89.062],[41.133,-17.98],[-15.733,-21.512],[-12.907,-67.005]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.481],"y":[1]},"o":{"x":[0.205],"y":[0.937]},"n":["0p481_1_0p205_0p937"],"t":1.199,"s":[45.333],"e":[0]},{"t":20.3796008300781}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.397],"y":[1]},"o":{"x":[0.203],"y":[0.708]},"n":["0p397_1_0p203_0p708"],"t":1.199,"s":[76.02],"e":[0]},{"t":20.3796008300781}],"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.23137254902,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":1.199,"s":[15],"e":[5]},{"t":20.3796008300781}],"ix":5},"lc":2,"lj":1,"ml":4,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1.19880004882812,"op":31.1688012695313,"st":-8.39160034179688,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-33,"ix":10},"p":{"a":0,"k":[325,324.5,0],"ix":2},"a":{"a":0,"k":[7,-45,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]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[24,24,100],"e":[100,100,100]},{"t":13.1868005371094}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[87.784,99.411],[-79.529,70.227],[-56.182,-63.623],[50.898,-44.945],[35.956,40.719],[-32.575,28.765],[-23.012,-26.06],[20.848,-18.41],[14.728,16.678],[-13.343,11.782]],"o":[[-99.411,87.784],[-70.227,-79.529],[63.623,-56.182],[44.945,50.899],[-40.719,35.956],[-28.765,-32.575],[26.06,-23.012],[18.41,20.848],[-16.678,14.728],[-11.782,-13.343],[0,0]],"v":[[204.587,130.304],[-134.36,109.251],[-117.518,-161.906],[99.408,-148.433],[88.63,25.108],[-50.203,16.485],[-43.305,-94.581],[45.548,-89.062],[41.133,-17.98],[-15.733,-21.512],[-12.907,-67.005]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.481],"y":[1]},"o":{"x":[0.205],"y":[0.937]},"n":["0p481_1_0p205_0p937"],"t":1.199,"s":[45.333],"e":[0]},{"t":20.3796008300781}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.397],"y":[1]},"o":{"x":[0.203],"y":[0.708]},"n":["0p397_1_0p203_0p708"],"t":1.199,"s":[76.02],"e":[0]},{"t":20.3796008300781}],"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.430208333333,0.185554534314,0.641207107843,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":1.199,"s":[15],"e":[5]},{"t":20.3796008300781}],"ix":5},"lc":2,"lj":1,"ml":4,"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":"Shape 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1.19880004882812,"op":31.1688012695313,"st":-8.39160034179688,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"mouth","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[512.576,422.723,0],"ix":2},"a":{"a":0,"k":[0,52.789,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[47.724,47.724,100],"e":[86.724,86.724,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":12,"s":[86.724,86.724,100],"e":[54.724,54.724,100]},{"t":22.0000008960784}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"n":"0p833_0p833_0p333_0","t":0,"s":[{"i":[[-9.958,0],[0,0.251],[9.958,0],[0,9.958]],"o":[[9.958,0],[0,9.958],[-9.958,0],[0,0.064]],"v":[[-0.187,0.094],[18.031,0],[0,18.031],[-18.031,0]],"c":true}],"e":[{"i":[[-9.958,0],[0,0.251],[9.958,0],[0,9.958]],"o":[[9.958,0],[0,9.958],[-9.958,0],[0,0.064]],"v":[[-0.187,0.094],[19.825,0.1],[-0.106,30.121],[-20.224,0.1]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":2,"s":[{"i":[[-9.958,0],[0,0.251],[9.958,0],[0,9.958]],"o":[[9.958,0],[0,9.958],[-9.958,0],[0,0.064]],"v":[[-0.187,0.094],[19.825,0.1],[-0.106,30.121],[-20.224,0.1]],"c":true}],"e":[{"i":[[-9.958,0],[0,0.251],[9.958,0],[0,9.958]],"o":[[9.958,0],[0,9.958],[-9.958,0],[0,0.064]],"v":[[-0.187,0.094],[18.284,0.362],[0,18.031],[-19.804,0.362]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":12,"s":[{"i":[[-9.958,0],[0,0.251],[9.958,0],[0,9.958]],"o":[[9.958,0],[0,9.958],[-9.958,0],[0,0.064]],"v":[[-0.187,0.094],[18.284,0.362],[0,18.031],[-19.804,0.362]],"c":true}],"e":[{"i":[[-9.958,0],[0,0.251],[9.958,0],[0,9.958]],"o":[[9.958,0],[0,9.958],[-9.958,0],[0,0.064]],"v":[[-0.187,0.094],[18.031,0],[0,18.031],[-18.031,0]],"c":true}]},{"t":22.0000008960784}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.6,0.576470588235,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":2,"lj":2,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.6,0.576470588235,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,43.773],"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,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120.0000048877,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"eye 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":5,"s":[488.576,401.877,0],"e":[488.576,384.877,0],"to":[0,-2.83333325386047,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":15,"s":[488.576,384.877,0],"e":[488.576,401.877,0],"to":[0,0,0],"ti":[0,-2.83333325386047,0]},{"t":25.0000010182709}],"ix":2},"a":{"a":0,"k":[24.295,13.627,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":4,"s":[100,100,100],"e":[100,131,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":12,"s":[100,131,100],"e":[100,91,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":27,"s":[100,91,100],"e":[100,100,100]},{"t":31.0000012626559}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[47.414,47.414],"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,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[24.295,13.627],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[49.291,49.291],"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 2","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120.0000048877,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"eye","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":5,"s":[536.863,401.877,0],"e":[536.863,384.877,0],"to":[0,-2.83333325386047,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":15,"s":[536.863,384.877,0],"e":[536.863,401.877,0],"to":[0,0,0],"ti":[0,-2.83333325386047,0]},{"t":25.0000010182709}],"ix":2},"a":{"a":0,"k":[24.295,13.627,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":4,"s":[100,100,100],"e":[100,131,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":12,"s":[100,131,100],"e":[100,91,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":27,"s":[100,91,100],"e":[100,100,100]},{"t":31.0000012626559}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[47.414,47.414],"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,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[24.295,13.627],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[49.291,49.291],"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 2","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120.0000048877,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"reflection 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.393],"y":[1]},"o":{"x":[0.151],"y":[0]},"n":["0p393_1_0p151_0"],"t":0,"s":[297.814],"e":[586]},{"t":37.0000015070409}],"ix":10},"p":{"a":0,"k":[512.576,384,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[85.833,85.833,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[343.109,343.109],"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,0.901960784314,0.901960784314,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":2,"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":[55.898,55.898],"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,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[26]},{"t":15.0000006109625}],"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.0000048877,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"reflection","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.393],"y":[1]},"o":{"x":[0.151],"y":[0]},"n":["0p393_1_0p151_0"],"t":0,"s":[157.869],"e":[403]},{"t":37.0000015070409}],"ix":10},"p":{"a":0,"k":[512.576,384,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[85.833,85.833,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[343.109,343.109],"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,0.901960784314,0.901960784314,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":2,"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":[55.898,55.898],"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,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[26]},{"t":15.0000006109625}],"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.0000048877,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"body","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[512.576,384,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":[343.109,343.109],"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,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":1,"lj":1,"ml":4,"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,"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":[52.945,52.945],"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,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120.0000048877,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"loops","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[512,384,0],"ix":2},"a":{"a":0,"k":[325,325,0],"ix":1},"s":{"a":0,"k":[51,51,100],"ix":6}},"ao":0,"w":650,"h":650,"ip":0,"op":29.9700012207031,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"leka","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.265],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p265_1_0p167_0p167"],"t":0,"s":[0],"e":[360]},{"t":22.0000008960784}],"ix":10},"p":{"a":0,"k":[512,384,0],"ix":2},"a":{"a":0,"k":[512,384,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]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"t":11.0000004480392}],"ix":6}},"ao":0,"w":1024,"h":768,"ip":0,"op":120.0000048877,"st":0,"bm":0}]} diff --git a/Modules/GameEngineKit/Sources/CurrentActivityManager.swift b/Modules/GameEngineKit/Sources/CurrentActivityManager.swift new file mode 100644 index 0000000000..92294b5fe5 --- /dev/null +++ b/Modules/GameEngineKit/Sources/CurrentActivityManager.swift @@ -0,0 +1,71 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit + +public class CurrentActivityManager { + // MARK: Lifecycle + + public init(activity: Activity) { + var copyOfActivity = activity + + if copyOfActivity.exercisePayload.options.shuffleExercises { + copyOfActivity.exercisePayload.exerciseGroups = copyOfActivity.exercisePayload.exerciseGroups.map { + Activity.ExercisesPayload.ExerciseGroup(exercises: $0.exercises.shuffled()) + } + } + + if copyOfActivity.exercisePayload.options.shuffleGroups { + copyOfActivity.exercisePayload.exerciseGroups.shuffle() + } + + self.activity = copyOfActivity + } + + // MARK: Public + + public var currentGroupIndex: Int = 0 + public var currentExerciseIndexInCurrentGroup: Int = 0 + + public let activity: Activity + + public var totalGroups: Int { + self.activity.exercisePayload.exerciseGroups.count + } + + public var totalExercisesInCurrentGroup: Int { + self.activity.exercisePayload.exerciseGroups[self.currentGroupIndex].exercises.count + } + + public var currentExercise: Exercise { + self.activity.exercisePayload.exerciseGroups[self.currentGroupIndex].exercises[self.currentExerciseIndexInCurrentGroup] + } + + public var isFirstExercise: Bool { + self.currentExerciseIndexInCurrentGroup == 0 && self.currentGroupIndex == 0 + } + + public var isLastExercise: Bool { + self.currentExerciseIndexInCurrentGroup == self.activity.exercisePayload.exerciseGroups[self.currentGroupIndex].exercises.count - 1 + && self.currentGroupIndex == self.activity.exercisePayload.exerciseGroups.count - 1 + } + + public func moveToNextExercise() { + if self.currentExerciseIndexInCurrentGroup < self.activity.exercisePayload.exerciseGroups[self.currentGroupIndex].exercises.count - 1 { + self.currentExerciseIndexInCurrentGroup += 1 + } else if self.currentGroupIndex < self.activity.exercisePayload.exerciseGroups.count - 1 { + self.currentGroupIndex += 1 + self.currentExerciseIndexInCurrentGroup = 0 + } + } + + public func moveToPreviousExercise() { + if self.currentExerciseIndexInCurrentGroup > 0 { + self.currentExerciseIndexInCurrentGroup -= 1 + } else if self.currentGroupIndex > 0 { + self.currentGroupIndex -= 1 + self.currentExerciseIndexInCurrentGroup = self.activity.exercisePayload.exerciseGroups[self.currentGroupIndex].exercises.count - 1 + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/ActionButton/ActionButton+Listen.swift b/Modules/GameEngineKit/Sources/Exercises/ActionButton/ActionButton+Listen.swift new file mode 100644 index 0000000000..2d635c19d4 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/ActionButton/ActionButton+Listen.swift @@ -0,0 +1,36 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import DesignKit +import SwiftUI + +struct ActionButtonListen: View { + @ObservedObject var audioPlayer: AudioPlayer + + var body: some View { + Button { + self.audioPlayer.play() + } label: { + Image(systemName: "speaker.2") + .font(.system(size: 100, weight: .medium)) + .foregroundColor(.accentColor) + .padding(40) + } + .frame(width: 200) + .disabled(self.audioPlayer.isPlaying) + .buttonStyle(ActionButtonStyle(progress: self.audioPlayer.progress)) + .scaleEffect(self.audioPlayer.isPlaying ? 1.0 : 0.8, anchor: .center) + .shadow( + color: .accentColor.opacity(0.2), + radius: self.audioPlayer.isPlaying ? 6 : 3, x: 0, y: 3 + ) + .animation(.spring(response: 0.3, dampingFraction: 0.45), value: self.audioPlayer.isPlaying) + } +} + +#Preview { + ActionButtonListen( + audioPlayer: AudioPlayer(audioRecording: AudioRecording(name: "drums", file: "drums"))) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/ActionButton/ActionButton+Observe.swift b/Modules/GameEngineKit/Sources/Exercises/ActionButton/ActionButton+Observe.swift new file mode 100644 index 0000000000..8d93d45397 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/ActionButton/ActionButton+Observe.swift @@ -0,0 +1,103 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - AnimatableBlur + +struct AnimatableBlur: AnimatableModifier { + var blurRadius: CGFloat + + var animatableData: CGFloat { + get { self.blurRadius } + set { self.blurRadius = newValue } + } + + func body(content: Content) -> some View { + content + .blur(radius: self.blurRadius) + } +} + +// MARK: - AnimatableSaturation + +struct AnimatableSaturation: AnimatableModifier { + var saturation: Double + + var animatableData: Double { + get { self.saturation } + set { self.saturation = newValue } + } + + func body(content: Content) -> some View { + content + .saturation(self.saturation) + } +} + +// MARK: - ActionButtonObserve + +struct ActionButtonObserve: View { + let image: String + + @Binding var imageWasTapped: Bool + + var body: some View { + if let uiImage = UIImage(named: image) { + Button { + withAnimation { + self.imageWasTapped = true + } + } label: { + VStack { + Image(systemName: "hand.tap") + .resizable() + .frame(width: 70, height: 70) + .foregroundColor(.white) + Text("Tap to reveal") + .font(.title) + .foregroundColor(.white) + } + .transition(.opacity) + .opacity(self.imageWasTapped ? 0 : 1) + .frame(width: 460, height: 460) + .contentShape(RoundedRectangle(cornerRadius: 10)) + } + .disabled(self.imageWasTapped) + .background { + Image(uiImage: uiImage) + .resizable() + .frame(width: 460, height: 460) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .modifier(AnimatableBlur(blurRadius: self.imageWasTapped ? 0 : 20)) + .modifier(AnimatableSaturation(saturation: self.imageWasTapped ? 1 : 0)) + } + } else { + Text("❌\nImage not found:\n\(self.image)") + .multilineTextAlignment(.center) + .overlay { + Circle() + .stroke(Color.red, lineWidth: 5) + } + .frame( + width: 460, + height: 460 + ) + } + } +} + +#Preview { + struct ActionObserveButtonContainer: View { + @State var imageWasTapped = false + var body: some View { + ActionButtonObserve( + image: "placeholder-observe_then_touch_to_select", imageWasTapped: $imageWasTapped + ) + } + } + + return ActionObserveButtonContainer() +} diff --git a/Modules/GameEngineKit/Sources/Exercises/ActionButton/ActionButton+Style.swift b/Modules/GameEngineKit/Sources/Exercises/ActionButton/ActionButton+Style.swift new file mode 100644 index 0000000000..a7b34ff9f3 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/ActionButton/ActionButton+Style.swift @@ -0,0 +1,58 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct ActionButtonStyle: ButtonStyle { + var progress: CGFloat + + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + .mask(Circle().inset(by: 4)) + .background( + Circle() + .fill( + Color.white, strokeBorder: DesignKitAsset.Colors.gameButtonBorder.swiftUIColor, + lineWidth: 4 + ) + .overlay( + Circle() + .trim(from: 0, to: self.progress) + .stroke(Color.accentColor, style: StrokeStyle(lineWidth: 10, lineCap: .round)) + .rotationEffect(.degrees(-90)) + .animation(.easeOut(duration: 0.2), value: self.progress) + ) + ) + .contentShape(Circle()) + } +} + +#Preview { + HStack(spacing: 80) { + Button { + print("Pressed!") + } label: { + Text("Press me") + .frame(width: 200, height: 200) + } + .buttonStyle(ActionButtonStyle(progress: 0.0)) + + Button { + print("Pressed!") + } label: { + Text("Press me") + .frame(width: 200, height: 200) + } + .buttonStyle(ActionButtonStyle(progress: 0.5)) + + Button { + print("Pressed!") + } label: { + Text("Press me") + .frame(width: 200, height: 200) + } + .buttonStyle(ActionButtonStyle(progress: 1.0)) + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/DraggableImageAnswerNode.swift b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/DraggableImageAnswerNode.swift new file mode 100644 index 0000000000..d39b383ce1 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/DraggableImageAnswerNode.swift @@ -0,0 +1,53 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SpriteKit + +class DraggableImageAnswerNode: SKSpriteNode { + // MARK: Lifecycle + + init(choice: GameplayAssociateCategoriesChoiceModel, scale: CGFloat = 1, position: CGPoint) { + self.id = choice.id + + super.init(texture: SKTexture(image: UIImage(named: choice.choice.value)!), color: .clear, size: CGSize.zero) + + let action = SKAction.setTexture(texture!, resize: true) + self.run(action) + + self.name = choice.choice.value + self.texture = texture + self.setScale(scale) + self.size = size + self.position = position + self.defaultPosition = position + } + + init(choice: GameplayDragAndDropIntoZonesChoiceModel, scale: CGFloat = 1, position: CGPoint) { + self.id = choice.id + + super.init(texture: SKTexture(image: UIImage(named: choice.choice.value)!), color: .clear, size: CGSize.zero) + + let action = SKAction.setTexture(texture!, resize: true) + self.run(action) + + self.name = choice.choice.value + self.texture = texture + self.setScale(scale) + self.size = size + self.position = position + self.defaultPosition = position + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + var id: String + var isDraggable: Bool = true + var defaultPosition: CGPoint? +} diff --git a/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/DraggableImageShadowNode.swift b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/DraggableImageShadowNode.swift new file mode 100644 index 0000000000..7536bcfa0c --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/DraggableImageShadowNode.swift @@ -0,0 +1,29 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SpriteKit + +class DraggableImageShadowNode: SKSpriteNode { + init(draggableImageAnswerNode: DraggableImageAnswerNode) { + super + .init( + texture: draggableImageAnswerNode.texture, color: draggableImageAnswerNode.color, + size: draggableImageAnswerNode.size + ) + + let actionShadow = SKAction.setTexture(draggableImageAnswerNode.texture!, resize: true) + run(actionShadow) + + blendMode = SKBlendMode.alpha + colorBlendFactor = 1.0 + color = .black + alpha = 0.15 + setScale(draggableImageAnswerNode.xScale) + position = draggableImageAnswerNode.position + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+0_BaseScene.swift b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+0_BaseScene.swift new file mode 100644 index 0000000000..c5635be47f --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+0_BaseScene.swift @@ -0,0 +1,279 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import SpriteKit +import SwiftUI + +extension DragAndDropIntoZonesView { + struct DropZoneNode { + let details: DragAndDropIntoZones.DropZone.Details + var node: SKSpriteNode = .init() + let zone: DragAndDropIntoZones.DropZone + } + + class BaseScene: SKScene { + // MARK: Lifecycle + + init( + viewModel: ViewModel, hints: Bool, dropZoneA: DragAndDropIntoZones.DropZone.Details, + dropZoneB: DragAndDropIntoZones.DropZone.Details? = nil + ) { + self.viewModel = viewModel + self.hints = hints + self.dropZoneA = DropZoneNode(details: dropZoneA, zone: .zoneA) + if let dropZoneB { + self.dropZoneB = DropZoneNode(details: dropZoneB, zone: .zoneB) + } + super.init(size: CGSize.zero) + self.spacer = size.width / CGFloat(viewModel.choices.count + 1) + self.defaultPosition = CGPoint(x: self.spacer, y: size.height) + + self.subscribeToChoicesUpdates() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + var viewModel: ViewModel + var dropZoneA: DropZoneNode + var dropZoneB: DropZoneNode? + + func reset() { + backgroundColor = .clear + removeAllChildren() + removeAllActions() + + self.setFirstAnswerPosition() + self.layoutDropZones() + self.getExpectedItems() + self.layoutAnswers() + } + + func exerciseCompletedBehavior() { + for node in self.answerNodes { + node.isDraggable = false + } + } + + func subscribeToChoicesUpdates() { + self.viewModel.$choices + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] choices in + guard let self else { return } + + for choice in choices where choice.id == self.playedNode?.id { + if choice.state == .rightAnswer { + self.goodAnswerBehavior(self.playedNode!) + } else if choice.state == .wrongAnswer { + self.wrongAnswerBehavior(self.playedNode!) + } + } + }) + .store(in: &self.cancellables) + } + + @MainActor func layoutAnswers() { + for choice in self.viewModel.choices { + let draggableImageAnswerNode = DraggableImageAnswerNode( + choice: choice, + position: defaultPosition + ) + let draggableImageShadowNode = DraggableImageShadowNode( + draggableImageAnswerNode: draggableImageAnswerNode + ) + + self.normalizeAnswerNodesSize([draggableImageAnswerNode, draggableImageShadowNode]) + self.bindNodesToSafeArea([draggableImageAnswerNode, draggableImageShadowNode]) + self.setNextAnswerPosition() + + self.answerNodes.append(draggableImageAnswerNode) + + addChild(draggableImageShadowNode) + addChild(draggableImageAnswerNode) + } + } + + func normalizeAnswerNodesSize(_ nodes: [SKSpriteNode]) { + for node in nodes { + node.scaleForMax(sizeOf: self.biggerSide) + } + } + + func bindNodesToSafeArea(_ nodes: [SKSpriteNode], limit: CGFloat = 80) { + let xRange = SKRange(lowerLimit: 0, upperLimit: size.width - limit) + let yRange = SKRange(lowerLimit: 0, upperLimit: size.height - limit) + for node in nodes { + node.constraints = [SKConstraint.positionX(xRange, y: yRange)] + } + } + + func setFirstAnswerPosition() { + self.spacer = size.width / CGFloat(self.viewModel.choices.count + 1) + self.defaultPosition = CGPoint(x: self.spacer, y: size.height) + } + + func setNextAnswerPosition() { + self.defaultPosition.x += self.spacer + } + + func layoutDropZones() { + fatalError("layoutDropZones(dropZones:) has not been implemented") + } + + func getExpectedItems() { + let index = self.viewModel.choices.firstIndex(where: { $0.choice.dropZone == .zoneA })! + let gameplayChoiceModel = self.viewModel.choices[index] + let expectedItem = gameplayChoiceModel.choice.value + let expectedNode = SKSpriteNode() + + guard self.hints else { + expectedNode.name = expectedItem + (self.expectedItemsNodes[self.dropZoneA.details.value, default: []]).append(expectedNode) + return + } + let texture = SKTexture(image: UIImage(named: expectedItem)!) + let action = SKAction.setTexture(texture, resize: true) + expectedNode.run(action) + expectedNode.name = expectedItem + expectedNode.texture = texture + expectedNode.scaleForMax(sizeOf: self.biggerSide * 0.8) + expectedNode.position = CGPoint(x: self.dropZoneA.node.position.x + 80, y: 110) + self.expectedItemsNodes[self.dropZoneA.details.value, default: []].append(expectedNode) + + addChild(expectedNode) + } + + func goodAnswerBehavior(_ node: DraggableImageAnswerNode) { + node.scaleForMax(sizeOf: self.biggerSide) + node.zPosition = 10 + node.isDraggable = false + self.onDropAction(node) + if case .completed = self.viewModel.exercicesSharedData.state { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [self] in + self.exerciseCompletedBehavior() + } + } + } + + func wrongAnswerBehavior(_ node: DraggableImageAnswerNode) { + let moveAnimation = SKAction.move(to: node.defaultPosition!, duration: 0.25) + .moveAnimation(.easeOut) + let group = DispatchGroup() + group.enter() + node.scaleForMax(sizeOf: self.biggerSide) + node.run( + moveAnimation, + completion: { + node.position = node.defaultPosition! + node.zPosition = 10 + group.leave() + } + ) + group.notify(queue: .main) { + self.onDropAction(node) + } + self.disableWrongAnswer(node) + } + + func onDragAnimation(_ node: SKSpriteNode) { + let wiggleAnimation = SKAction.sequence([ + SKAction.rotate(byAngle: CGFloat(degreesToRadian(degrees: -4)), duration: 0.1), + SKAction.rotate(byAngle: 0.0, duration: 0.1), + SKAction.rotate(byAngle: CGFloat(degreesToRadian(degrees: 4)), duration: 0.1), + ]) + node.scaleForMax(sizeOf: self.biggerSide * 1.1) + node.run(SKAction.repeatForever(wiggleAnimation)) + } + + func onDropAction(_ node: SKSpriteNode) { + node.zRotation = 0 + node.removeAllActions() + self.selectedNodes = [:] + } + + override func didMove(to _: SKView) { + self.reset() + } + + // overriden Touches states + override func touchesBegan(_ touches: Set, with _: UIEvent?) { + for touch in touches { + let location = touch.location(in: self) + if let node = atPoint(location) as? DraggableImageAnswerNode { + for choice in self.viewModel.choices + where node.id == choice.id && node.isDraggable + { + selectedNodes[touch] = node + onDragAnimation(node) + node.zPosition += 100 + } + } + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + for touch in touches { + let location = touch.location(in: self) + if let node = selectedNodes[touch] { + let bounds: CGRect = view!.bounds + if node.fullyContains(location: location, bounds: bounds) { + node.run(SKAction.move(to: location, duration: 0.05).moveAnimation(.linear)) + node.position = location + } else { + self.touchesEnded(touches, with: event) + } + } + } + } + + override func touchesEnded(_ touches: Set, with _: UIEvent?) { + for touch in touches { + guard self.selectedNodes.keys.contains(touch) else { + break + } + self.playedNode = self.selectedNodes[touch]! + self.playedNode!.scaleForMax(sizeOf: self.biggerSide) + let gameplayChoiceModel = self.viewModel.choices.first(where: { $0.id == self.playedNode!.id }) + + if self.playedNode!.fullyContains(bounds: self.dropZoneA.node.frame) { + self.viewModel.onChoiceTapped(choice: gameplayChoiceModel!, dropZone: self.dropZoneA.zone) + break + } + + if let dropZoneB, self.playedNode!.fullyContains(bounds: dropZoneB.node.frame) { + self.viewModel.onChoiceTapped(choice: gameplayChoiceModel!, dropZone: dropZoneB.zone) + break + } + + self.wrongAnswerBehavior(self.playedNode!) + } + } + + // MARK: Private + + private var hints: Bool + private var biggerSide: CGFloat = 140 + private var selectedNodes: [UITouch: DraggableImageAnswerNode] = [:] + private var answerNodes: [DraggableImageAnswerNode] = [] + private var playedNode: DraggableImageAnswerNode? + private var spacer: CGFloat = .zero + private var defaultPosition = CGPoint.zero + private var expectedItemsNodes: [String: [SKSpriteNode]] = [:] + private var cancellables: Set = [] + + private func disableWrongAnswer(_ node: DraggableImageAnswerNode) { + let gameplayChoiceModel = self.viewModel.choices.first(where: { $0.id == node.id })! + if gameplayChoiceModel.choice.dropZone == nil { + node.colorBlendFactor = 0.4 + node.isDraggable = false + } + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+1_OneZoneScene.swift b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+1_OneZoneScene.swift new file mode 100644 index 0000000000..f95b59f01c --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+1_OneZoneScene.swift @@ -0,0 +1,24 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SpriteKit + +extension DragAndDropIntoZonesView { + final class OneZoneScene: DragAndDropIntoZonesView.BaseScene { + override func layoutDropZones() { + // TODO(@hugo): Add type declaration + let dropZoneNode = SKSpriteNode() + let dropZoneSize = CGSize(width: 380, height: 280) + + dropZoneNode.size = dropZoneSize + dropZoneNode.texture = SKTexture(imageNamed: dropZoneA.details.value) + dropZoneNode.position = CGPoint(x: size.width / 2, y: dropZoneSize.height / 2) + dropZoneNode.name = dropZoneA.details.value + + addChild(dropZoneNode) + + dropZoneA.node = dropZoneNode + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+2_TwoZonesScene.swift b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+2_TwoZonesScene.swift new file mode 100644 index 0000000000..49ae5bc750 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+2_TwoZonesScene.swift @@ -0,0 +1,38 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SpriteKit + +extension DragAndDropIntoZonesView { + final class TwoZonesScene: DragAndDropIntoZonesView.BaseScene { + override func layoutDropZones() { + guard let unwrappedDropZoneB = dropZoneB else { + fatalError("No dropZoneB provided") + } + + // TODO(@hugo): Add type declaration + let dropZoneNodeA = SKSpriteNode() + let dropZoneNodeB = SKSpriteNode() + + let dropZoneSize = CGSize(width: 410, height: 320) + dropZoneNodeA.size = dropZoneSize + dropZoneNodeB.size = dropZoneSize + + dropZoneNodeA.texture = SKTexture(imageNamed: dropZoneA.details.value) + dropZoneNodeB.texture = SKTexture(imageNamed: unwrappedDropZoneB.details.value) + + let dropZonePosition = size.width / 4 + dropZoneNodeA.position = CGPoint(x: dropZonePosition, y: dropZoneSize.height * 5 / 7) + dropZoneNodeB.position = CGPoint(x: dropZonePosition * 3, y: dropZoneSize.height * 5 / 7) + + dropZoneNodeA.name = dropZoneA.details.value + dropZoneNodeB.name = unwrappedDropZoneB.details.value + addChild(dropZoneNodeA) + addChild(dropZoneNodeB) + + dropZoneA.node = dropZoneNodeA + dropZoneB?.node = dropZoneNodeB + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+ViewModel.swift b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+ViewModel.swift new file mode 100644 index 0000000000..3aa37c1273 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView+ViewModel.swift @@ -0,0 +1,60 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import SwiftUI + +extension DragAndDropIntoZonesView { + class ViewModel: ObservableObject { + // MARK: Lifecycle + + init(choices: [DragAndDropIntoZones.Choice], shared: ExerciseSharedData? = nil) { + let gameplayChoiceModel = choices.map { GameplayDragAndDropIntoZonesChoiceModel(choice: $0) } + self.choices = gameplayChoiceModel + self.gameplay = GameplayFindTheRightAnswers( + choices: gameplayChoiceModel) + self.exercicesSharedData = shared ?? ExerciseSharedData() + + self.subscribeToGameplayDragAndDropChoicesUpdates() + self.subscribeToGameplayStateUpdates() + } + + // MARK: Public + + public func onChoiceTapped( + choice: GameplayDragAndDropIntoZonesChoiceModel, dropZone: DragAndDropIntoZones.DropZone + ) { + self.gameplay.process(choice, dropZone) + } + + // MARK: Internal + + @Published var choices: [GameplayDragAndDropIntoZonesChoiceModel] = [] + @ObservedObject var exercicesSharedData: ExerciseSharedData + + // MARK: Private + + private let gameplay: GameplayFindTheRightAnswers + private var cancellables: Set = [] + + private func subscribeToGameplayDragAndDropChoicesUpdates() { + self.gameplay.choices + .receive(on: DispatchQueue.main) + .sink { + self.choices = $0 + } + .store(in: &self.cancellables) + } + + private func subscribeToGameplayStateUpdates() { + self.gameplay.state + .receive(on: DispatchQueue.main) + .sink { + self.exercicesSharedData.state = $0 + } + .store(in: &self.cancellables) + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView.swift b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView.swift new file mode 100644 index 0000000000..23a32dbf2c --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/IntoZones/DragAndDropIntoZonesView.swift @@ -0,0 +1,80 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import SpriteKit +import SwiftUI + +public struct DragAndDropIntoZonesView: View { + // MARK: Lifecycle + + // TODO(@HPezz): Add hints variable + // let hints: Bool + + public init( + choices: [DragAndDropIntoZones.Choice], dropZoneA: DragAndDropIntoZones.DropZone.Details, + dropZoneB: DragAndDropIntoZones.DropZone.Details? = nil + ) { + _viewModel = StateObject(wrappedValue: ViewModel(choices: choices)) + self.dropZoneA = dropZoneA + self.dropZoneB = dropZoneB + } + + public init(exercise: Exercise, data: ExerciseSharedData? = nil) { + guard let payload = exercise.payload as? DragAndDropIntoZones.Payload else { + log.error("Exercise payload is not .dragAndDrop") + fatalError("💥 Exercise payload is not .dragAndDrop") + } + + _viewModel = StateObject( + wrappedValue: ViewModel(choices: payload.choices, shared: data)) + + self.dropZoneA = payload.dropZoneA + self.dropZoneB = payload.dropZoneB + } + + // MARK: Public + + public var body: some View { + GeometryReader { proxy in + SpriteView( + scene: self.makeScene(size: proxy.size), + options: [.allowsTransparency] + ) + .frame(width: proxy.size.width, height: proxy.size.height) + .onAppear { + if let dropZoneB { + self.scene = DragAndDropIntoZonesView.TwoZonesScene( + viewModel: self.viewModel, hints: false, dropZoneA: self.dropZoneA, dropZoneB: dropZoneB + ) + } else { + self.scene = DragAndDropIntoZonesView.OneZoneScene( + viewModel: self.viewModel, hints: true, dropZoneA: self.dropZoneA + ) + } + } + } + .edgesIgnoringSafeArea(.horizontal) + } + + // MARK: Internal + + let dropZoneA: DragAndDropIntoZones.DropZone.Details + let dropZoneB: DragAndDropIntoZones.DropZone.Details? + + // MARK: Private + + @StateObject private var viewModel: ViewModel + @State private var scene: SKScene = .init() + + private func makeScene(size: CGSize) -> SKScene { + guard let finalScene = scene as? DragAndDropIntoZonesView.BaseScene else { + return SKScene() + } + finalScene.size = CGSize(width: size.width, height: size.height) + finalScene.viewModel = self.viewModel + return finalScene + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+0_BaseScene.swift b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+0_BaseScene.swift new file mode 100644 index 0000000000..9473d7dfa3 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+0_BaseScene.swift @@ -0,0 +1,229 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import SpriteKit +import SwiftUI + +extension DragAndDropToAssociateView { + class BaseScene: SKScene { + // MARK: Lifecycle + + init(viewModel: ViewModel) { + self.viewModel = viewModel + super.init(size: CGSize.zero) + + self.subscribeToChoicesUpdates() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + var viewModel: ViewModel + var spacer = CGFloat.zero + var defaultPosition = CGPoint.zero + var initialNodeX: CGFloat = .zero + var verticalSpacing: CGFloat = .zero + + func reset() { + backgroundColor = .clear + removeAllChildren() + removeAllActions() + + self.dropDestinations = [] + + self.setFirstAnswerPosition() + self.layoutAnswers() + } + + func exerciseCompletedBehavior() { + for node in self.dropDestinations { + node.isDraggable = false + } + } + + func subscribeToChoicesUpdates() { + self.viewModel.$choices + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] choices in + guard let self else { return } + for choice in choices where choice.id == self.playedNode?.id { + if choice.state == .rightAnswer { + self.goodAnswerBehavior(self.playedNode!) + } else if choice.state == .wrongAnswer { + self.wrongAnswerBehavior(self.playedNode!) + } + } + }) + .store(in: &self.cancellables) + } + + @MainActor func layoutAnswers() { + for (index, gameplayChoiceModel) in self.viewModel.choices.enumerated() { + let draggableImageAnswerNode = DraggableImageAnswerNode( + choice: gameplayChoiceModel, + position: defaultPosition + ) + let draggableImageShadowNode = DraggableImageShadowNode( + draggableImageAnswerNode: draggableImageAnswerNode + ) + + self.normalizeAnswerNodesSize([draggableImageAnswerNode, draggableImageShadowNode]) + self.bindNodesToSafeArea([draggableImageAnswerNode, draggableImageShadowNode]) + self.setNextAnswerPosition(index) + + addChild(draggableImageShadowNode) + addChild(draggableImageAnswerNode) + + self.dropDestinations.append(draggableImageAnswerNode) + } + } + + func normalizeAnswerNodesSize(_ nodes: [SKSpriteNode]) { + for node in nodes { + node.scaleForMax(sizeOf: self.biggerSide) + } + } + + func bindNodesToSafeArea(_ nodes: [SKSpriteNode], limit: CGFloat = 80) { + let xRange = SKRange(lowerLimit: 0, upperLimit: size.width - limit) + let yRange = SKRange(lowerLimit: 0, upperLimit: size.height - limit) + for node in nodes { + node.constraints = [SKConstraint.positionX(xRange, y: yRange)] + } + } + + func setFirstAnswerPosition() { + fatalError("setFirstAnswerPosition() has not been implemented") + } + + func setNextAnswerPosition(_: Int) { + fatalError("setNextAnswerPosition(_ index:) has not been implemented") + } + + func goodAnswerBehavior(_ node: DraggableImageAnswerNode) { + node.scaleForMax(sizeOf: self.biggerSide * 0.8) + node.zPosition = (self.playedDestination?.zPosition ?? 10) + 10 + node.isDraggable = false + self.playedDestination?.isDraggable = false + self.onDropAction(node) + if case .completed = self.viewModel.exercicesSharedData.state { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [self] in + self.exerciseCompletedBehavior() + } + } + } + + func wrongAnswerBehavior(_ node: DraggableImageAnswerNode) { + let moveAnimation = SKAction.move(to: node.defaultPosition!, duration: 0.25) + .moveAnimation(.easeOut) + let group = DispatchGroup() + group.enter() + node.scaleForMax(sizeOf: self.biggerSide) + node.run( + moveAnimation, + completion: { + node.position = node.defaultPosition! + node.zPosition = 10 + group.leave() + } + ) + group.notify(queue: .main) { + self.onDropAction(node) + } + } + + func onDragAnimation(_ node: SKSpriteNode) { + let wiggleAnimation = SKAction.sequence([ + SKAction.rotate(byAngle: CGFloat(degreesToRadian(degrees: -4)), duration: 0.1), + SKAction.rotate(byAngle: 0.0, duration: 0.1), + SKAction.rotate(byAngle: CGFloat(degreesToRadian(degrees: 4)), duration: 0.1), + ]) + node.scaleForMax(sizeOf: self.biggerSide * 1.1) + node.run(SKAction.repeatForever(wiggleAnimation)) + } + + func onDropAction(_ node: SKSpriteNode) { + node.zRotation = 0 + node.removeAllActions() + self.selectedNodes = [:] + } + + override func didMove(to _: SKView) { + self.reset() + } + + // overriden Touches states + override func touchesBegan(_ touches: Set, with _: UIEvent?) { + for touch in touches { + let location = touch.location(in: self) + if let node = atPoint(location) as? DraggableImageAnswerNode { + for choice in self.viewModel + .choices where node.id == choice.id && node.isDraggable + { + selectedNodes[touch] = node + onDragAnimation(node) + node.zPosition += 100 + } + } + } + } + + override func touchesMoved(_ touches: Set, with _: UIEvent?) { + for touch in touches { + let location = touch.location(in: self) + if let node = selectedNodes[touch] { + let bounds: CGRect = view!.bounds + if node.fullyContains(location: location, bounds: bounds) { + node.run(SKAction.move(to: location, duration: 0.05).moveAnimation(.linear)) + node.position = location + } else { + self.wrongAnswerBehavior(node) + } + } + } + } + + override func touchesEnded(_ touches: Set, with _: UIEvent?) { + for touch in touches { + guard self.selectedNodes.keys.contains(touch) else { + break + } + self.playedNode = self.selectedNodes[touch]! + self.playedNode!.scaleForMax(sizeOf: self.biggerSide) + + guard let destinationNode = dropDestinations.first(where: { + $0.frame.contains(touch.location(in: self)) && $0.id != playedNode!.id + }) + else { + self.wrongAnswerBehavior(self.playedNode!) + break + } + self.playedDestination = destinationNode + + guard let destination = viewModel.choices.first(where: { $0.id == destinationNode.id }) + else { return } + guard let choice = viewModel.choices.first(where: { $0.id == playedNode!.id }) + else { return } + + self.viewModel.onChoiceTapped(choice: choice, destination: destination) + } + } + + // MARK: Private + + private var biggerSide: CGFloat = 150 + private var playedNode: DraggableImageAnswerNode? + private var playedDestination: DraggableImageAnswerNode? + private var dropDestinations: [DraggableImageAnswerNode] = [] + private var selectedNodes: [UITouch: DraggableImageAnswerNode] = [:] + private var expectedItemsNodes: [String: [SKSpriteNode]] = [:] + private var cancellables: Set = [] + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+2_TwoChoicesScene.swift b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+2_TwoChoicesScene.swift new file mode 100644 index 0000000000..4041849164 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+2_TwoChoicesScene.swift @@ -0,0 +1,18 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SpriteKit + +extension DragAndDropToAssociateView { + final class TwoChoicesScene: BaseScene { + override func setFirstAnswerPosition() { + spacer = size.width / CGFloat(viewModel.choices.count + 1) + defaultPosition = CGPoint(x: spacer, y: size.height / 2) + } + + override func setNextAnswerPosition(_: Int) { + defaultPosition.x += spacer + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+3_ThreeChoicesScene.swift b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+3_ThreeChoicesScene.swift new file mode 100644 index 0000000000..be5ca14ca9 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+3_ThreeChoicesScene.swift @@ -0,0 +1,25 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SpriteKit + +extension DragAndDropToAssociateView { + final class ThreeChoicesScene: BaseScene { + override func setFirstAnswerPosition() { + spacer = 455 + initialNodeX = (size.width - spacer) / 2 + verticalSpacing = size.height / 3 + defaultPosition = CGPoint(x: initialNodeX, y: (verticalSpacing * 2) + 30) + } + + override func setNextAnswerPosition(_ index: Int) { + if index == 1 { + defaultPosition.x = size.width / 2 + defaultPosition.y -= verticalSpacing + 60 + } else { + defaultPosition.x += spacer + } + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+4_FourChoicesScene.swift b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+4_FourChoicesScene.swift new file mode 100644 index 0000000000..8ed96b5653 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+4_FourChoicesScene.swift @@ -0,0 +1,25 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SpriteKit + +extension DragAndDropToAssociateView { + final class FourChoicesScene: BaseScene { + override func setFirstAnswerPosition() { + spacer = 455 + initialNodeX = (size.width - spacer) / 2 + verticalSpacing = size.height / 3 + defaultPosition = CGPoint(x: initialNodeX, y: verticalSpacing - 30) + } + + override func setNextAnswerPosition(_ index: Int) { + if [0, 2].contains(index) { + defaultPosition.x += spacer + } else { + defaultPosition.x = initialNodeX + defaultPosition.y += verticalSpacing + 60 + } + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+5_FiveChoicesScene.swift b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+5_FiveChoicesScene.swift new file mode 100644 index 0000000000..46f285f0d5 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+5_FiveChoicesScene.swift @@ -0,0 +1,25 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SpriteKit + +extension DragAndDropToAssociateView { + final class FiveChoicesScene: BaseScene { + override func setFirstAnswerPosition() { + spacer = 340 + initialNodeX = (size.width / 2) - spacer + verticalSpacing = size.height / 3 + defaultPosition = CGPoint(x: initialNodeX, y: (verticalSpacing * 2) + 30) + } + + override func setNextAnswerPosition(_ index: Int) { + if index == 2 { + defaultPosition.x = (size.width - spacer) / 2 + defaultPosition.y -= verticalSpacing + 60 + } else { + defaultPosition.x += spacer + } + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+6_SixChoicesScene.swift b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+6_SixChoicesScene.swift new file mode 100644 index 0000000000..22e9201c5a --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+6_SixChoicesScene.swift @@ -0,0 +1,25 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SpriteKit + +extension DragAndDropToAssociateView { + final class SixChoicesScene: BaseScene { + override func setFirstAnswerPosition() { + spacer = 340 + initialNodeX = (size.width / 2) - spacer + verticalSpacing = size.height / 3 + defaultPosition = CGPoint(x: initialNodeX, y: verticalSpacing - 30) + } + + override func setNextAnswerPosition(_ index: Int) { + if [0, 1, 3, 4].contains(index) { + defaultPosition.x += spacer + } else { + defaultPosition.x = initialNodeX + defaultPosition.y += verticalSpacing + 60 + } + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+ViewModel.swift b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+ViewModel.swift new file mode 100644 index 0000000000..557a599a67 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView+ViewModel.swift @@ -0,0 +1,63 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import SwiftUI + +extension DragAndDropToAssociateView { + class ViewModel: ObservableObject { + // MARK: Lifecycle + + init( + choices: [DragAndDropToAssociate.Choice], + shuffle: Bool = false, + shared: ExerciseSharedData? = nil + ) { + let gameplayChoiceModel = choices.map { GameplayAssociateCategoriesChoiceModel(choice: $0) } + self.choices = shuffle ? gameplayChoiceModel.shuffled() : gameplayChoiceModel + self.gameplay = GameplayAssociateCategories(choices: gameplayChoiceModel, shuffle: shuffle) + self.exercicesSharedData = shared ?? ExerciseSharedData() + + self.subscribeToGameplayAssociateCategoriesChoicesUpdates() + self.subscribeToGameplayStateUpdates() + } + + // MARK: Public + + public func onChoiceTapped( + choice: GameplayAssociateCategoriesChoiceModel, destination: GameplayAssociateCategoriesChoiceModel + ) { + self.gameplay.process(choice, destination) + } + + // MARK: Internal + + @Published var choices: [GameplayAssociateCategoriesChoiceModel] = [] + @ObservedObject var exercicesSharedData: ExerciseSharedData + + // MARK: Private + + private let gameplay: GameplayAssociateCategories + private var cancellables: Set = [] + + private func subscribeToGameplayAssociateCategoriesChoicesUpdates() { + self.gameplay.choices + .receive(on: DispatchQueue.main) + .sink { + self.choices = $0 + } + .store(in: &self.cancellables) + } + + private func subscribeToGameplayStateUpdates() { + self.gameplay.state + .receive(on: DispatchQueue.main) + .sink { + self.exercicesSharedData.state = $0 + } + .store(in: &self.cancellables) + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView.swift b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView.swift new file mode 100644 index 0000000000..bd9af3d432 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/DragAndDrop/ToAssociate/DragAndDropToAssociateView.swift @@ -0,0 +1,86 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import LogKit +import SpriteKit +import SwiftUI + +public struct DragAndDropToAssociateView: View { + // MARK: Lifecycle + + public init(choices: [DragAndDropToAssociate.Choice], shuffle: Bool = false) { + _viewModel = StateObject( + wrappedValue: ViewModel(choices: choices, shuffle: shuffle) + ) + } + + public init(exercise: Exercise, data: ExerciseSharedData? = nil) { + guard let payload = exercise.payload as? DragAndDropToAssociate.Payload else { + fatalError("Exercise payload is not .association") + } + + _viewModel = StateObject( + wrappedValue: ViewModel( + choices: payload.choices, + shuffle: payload.shuffleChoices, + shared: data + ) + ) + } + + // MARK: Public + + public var body: some View { + GeometryReader { proxy in + SpriteView( + scene: self.makeScene(size: proxy.size), + options: [.allowsTransparency] + ) + .frame(width: proxy.size.width, height: proxy.size.height) + .onAppear { + guard let interface = Interface(rawValue: viewModel.choices.count) else { return } + + switch interface { + case .twoChoices: + self.scene = TwoChoicesScene(viewModel: self.viewModel) + case .threeChoices: + self.scene = ThreeChoicesScene(viewModel: self.viewModel) + case .fourChoices: + self.scene = FourChoicesScene(viewModel: self.viewModel) + case .fiveChoices: + self.scene = FiveChoicesScene(viewModel: self.viewModel) + case .sixChoices: + self.scene = SixChoicesScene(viewModel: self.viewModel) + } + } + } + .edgesIgnoringSafeArea(.horizontal) + } + + // MARK: Internal + + enum Interface: Int { + case twoChoices = 2 + case threeChoices + case fourChoices + case fiveChoices + case sixChoices + } + + // MARK: Private + + @StateObject private var viewModel: ViewModel + @State private var scene: SKScene = .init() + + private func makeScene(size: CGSize) -> SKScene { + guard let finalScene = scene as? BaseScene else { + return SKScene() + } + finalScene.size = CGSize(width: size.width, height: size.height) + finalScene.viewModel = self.viewModel + return finalScene + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/ExerciseSharedData.swift b/Modules/GameEngineKit/Sources/Exercises/ExerciseSharedData.swift new file mode 100644 index 0000000000..a1beac1a44 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/ExerciseSharedData.swift @@ -0,0 +1,45 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +public class ExerciseSharedData: ObservableObject { + // MARK: Lifecycle + + public init(groupIndex: Int, exerciseIndex: Int) { + self.groupIndex = groupIndex + self.exerciseIndex = exerciseIndex + } + + public init() { + self.exerciseIndex = 0 + self.groupIndex = 0 + } + + // MARK: Internal + + // TODO: (@HPezz): Add state setter function, and makes it private + @Published var state: ExerciseState = .idle + + let groupIndex: Int + let exerciseIndex: Int + + var completionLevel: ExerciseState.CompletionLevel? { + guard case let .completed(level) = state else { return nil } + return level + } + + var isCompleted: Bool { + self.state.isCompleted + } + + var isExerciseNotYetCompleted: Bool { + switch self.state { + case .completed: + false + default: + true + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/ExerciseState.swift b/Modules/GameEngineKit/Sources/Exercises/ExerciseState.swift new file mode 100644 index 0000000000..53a5a3e67b --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/ExerciseState.swift @@ -0,0 +1,27 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +enum ExerciseState: Equatable { + case idle + case playing + case completed(level: CompletionLevel) + + // MARK: Internal + + enum CompletionLevel { + case fail + case belowAverage + case average + case good + case excellent + case nonApplicable + } + + var isCompleted: Bool { + switch self { + case .completed: true + default: false + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+ButtonStyles.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+ButtonStyles.swift new file mode 100644 index 0000000000..d2cda4ff27 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+ButtonStyles.swift @@ -0,0 +1,46 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +extension DanceFreezeView { + struct StageModeButtonStyle: View { + // MARK: Lifecycle + + init(_ text: String, color: Color) { + self.text = text + self.color = color + } + + // MARK: Internal + + let text: String + let color: Color + + var body: some View { + Text(self.text) + .font(.body) + .foregroundColor(.white) + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.center) + .frame(width: 220, height: 50) + .scaledToFit() + .background(RoundedRectangle(cornerRadius: 10).fill(self.color).shadow(radius: 3)) + } + } + + struct MotionModeButtonStyle: View { + let image: Image + let color: Color + + var body: some View { + self.image + .resizable() + .renderingMode(.template) + .foregroundStyle(self.color) + .scaledToFit() + .frame(maxWidth: 100) + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+DanceView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+DanceView.swift new file mode 100644 index 0000000000..ac553021fb --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+DanceView.swift @@ -0,0 +1,29 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import Lottie +import SwiftUI + +extension DanceFreezeView { + struct DanceView: View { + // MARK: Internal + + var body: some View { + LottieView( + animation: self.animation, + speed: 0.5, + loopMode: .loop + ) + } + + // MARK: Private + + private let animation = LottieAnimation.named("dance_freeze_dance.animation.lottie", bundle: .module)! + } +} + +#Preview { + DanceFreezeView.DanceView() +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+FreezeView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+FreezeView.swift new file mode 100644 index 0000000000..711ae77cd9 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+FreezeView.swift @@ -0,0 +1,29 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import Lottie +import SwiftUI + +extension DanceFreezeView { + struct FreezeView: View { + // MARK: Internal + + var body: some View { + LottieView( + animation: self.animation, + speed: 0.5, + loopMode: .loop + ) + } + + // MARK: Private + + private let animation = LottieAnimation.named("dance_freeze_freeze.animation.lottie", bundle: .module)! + } +} + +#Preview { + DanceFreezeView.FreezeView() +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+LauncherView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+LauncherView.swift new file mode 100644 index 0000000000..4f2ca55fde --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+LauncherView.swift @@ -0,0 +1,75 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import LocalizationKit +import SwiftUI + +extension DanceFreezeView { + struct LauncherView: View { + @Binding var mode: Stage + @Binding var motion: Motion + @Binding var selectedAudioRecording: AudioRecording + let songs: [AudioRecording] + + var body: some View { + VStack(spacing: 100) { + Text(l10n.DanceFreezeView.instructions) + .font(.headline) + .padding(.top, 30) + + HStack(spacing: 30) { + VStack(spacing: 0) { + GameEngineKitAsset.Exercises.DanceFreeze.imageIllustration.swiftUIImage + .resizable() + .aspectRatio(contentMode: .fit) + } + + VStack(spacing: 0) { + MotionSelectorView(motion: self.$motion) + + SongSelectorView( + songs: self.songs, + selectedAudioRecording: self.$selectedAudioRecording + ) + } + } + .padding(.horizontal, 100) + + HStack(spacing: 70) { + Button { + self.mode = .manualMode + } label: { + StageModeButtonStyle(String(l10n.DanceFreezeView.manualButtonLabel.characters), color: .cyan) + } + + Button { + self.mode = .automaticMode + } label: { + StageModeButtonStyle(String(l10n.DanceFreezeView.autoButtonLabel.characters), color: .mint) + } + } + .padding(.bottom, 30) + } + } + } +} + +#Preview { + let songs = [ + AudioRecording(name: "Giggly Squirrel", file: "Giggly_Squirrel"), + AudioRecording(name: "Empty Page", file: "Empty_Page"), + AudioRecording(name: "Early Bird", file: "Early_Bird"), + AudioRecording(name: "Hands On", file: "Hands_On"), + AudioRecording(name: "In The Game", file: "In_The_Game"), + AudioRecording(name: "Little by Little", file: "Little_by_little"), + ] + + return DanceFreezeView.LauncherView( + mode: .constant(.waitingForSelection), + motion: .constant(.rotation), + selectedAudioRecording: .constant(AudioRecording(.gigglySquirrel)), + songs: songs + ) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+MotionSelectorView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+MotionSelectorView.swift new file mode 100644 index 0000000000..dbf6246196 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+MotionSelectorView.swift @@ -0,0 +1,64 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import DesignKit +import LocalizationKit +import SwiftUI + +extension DanceFreezeView { + struct MotionSelectorView: View { + // MARK: Lifecycle + + init(motion: Binding) { + self._motion = motion + } + + // MARK: Internal + + var body: some View { + VStack { + HStack(spacing: 70) { + VStack(spacing: 0) { + MotionModeButtonStyle( + image: GameEngineKitAsset.Exercises.DanceFreeze.iconMotionModeRotation.swiftUIImage, + color: self.motion == .rotation ? .teal : .primary + ) + Text(l10n.DanceFreezeView.rotationButtonLabel) + } + .foregroundStyle(self.motion == .rotation ? .teal : .primary) + .onTapGesture { + withAnimation { + self.motion = .rotation + } + } + + VStack(spacing: 0) { + MotionModeButtonStyle( + image: GameEngineKitAsset.Exercises.DanceFreeze.iconMotionModeMovement.swiftUIImage, + color: self.motion == .movement ? .teal : .primary + ) + Text(l10n.DanceFreezeView.movementButtonLabel) + } + .foregroundStyle(self.motion == .movement ? .teal : .primary) + .onTapGesture { + withAnimation { + self.motion = .movement + } + } + } + } + .padding(.vertical, 15) + .padding(.horizontal, 40) + } + + // MARK: Private + + @Binding private var motion: Motion + } +} + +#Preview { + DanceFreezeView.MotionSelectorView(motion: .constant(.rotation)) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+PlayerView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+PlayerView.swift new file mode 100644 index 0000000000..3afd4dfeea --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+PlayerView.swift @@ -0,0 +1,71 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension DanceFreezeView { + struct PlayerView: View { + // MARK: Lifecycle + + public init(selectedAudioRecording: AudioRecording, isAuto: Bool, motion: Motion, data: ExerciseSharedData? = nil) { + self._viewModel = StateObject(wrappedValue: ViewModel(selectedAudioRecording: selectedAudioRecording, motion: motion, shared: data)) + self.isAuto = isAuto + self.motion = motion + } + + // MARK: Internal + + let isAuto: Bool + let motion: Motion + + var body: some View { + VStack { + ContinuousProgressBar(progress: self.viewModel.progress) + .padding(20) + + Button { + self.viewModel.onDanceFreezeToggle() + } label: { + if self.viewModel.isDancing { + DanceView() + } else { + FreezeView() + } + } + .disabled(self.isAuto) + } + .onAppear { + self.viewModel.onDanceFreezeToggle() + if self.isAuto { + self.randomSwitch() + } + } + .onDisappear { + self.viewModel.completeDanceFreeze() + } + } + + func randomSwitch() { + if case .completed = self.viewModel.exercicesSharedData.state { return } + if self.viewModel.progress < 1.0 { + let rand = Double.random(in: 2..<10) + + DispatchQueue.main.asyncAfter(deadline: .now() + rand) { + if case .completed = self.viewModel.exercicesSharedData.state { return } + self.viewModel.onDanceFreezeToggle() + self.randomSwitch() + } + } + } + + // MARK: Private + + @StateObject private var viewModel: ViewModel + } +} + +#Preview { + DanceFreezeView.PlayerView(selectedAudioRecording: AudioRecording(.earlyBird), isAuto: true, motion: .movement) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+RobotManager.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+RobotManager.swift new file mode 100644 index 0000000000..d7b35b42ca --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+RobotManager.swift @@ -0,0 +1,81 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import RobotKit +import SwiftUI + +extension DanceFreezeView { + class RobotManager { + // MARK: Internal + + let robot = Robot.shared + var lastMove = 0 + + func stopRobot() { + self.robot.blacken(.all) + self.robot.stopMotion() + } + + func freeze() { + self.robot.shine(.all(in: .white)) + self.robot.stopMotion() + } + + func shineRandomly() { + let randomColor = self.getRandomColor() + let randomLight = self.getRandomLight(color: randomColor) + + self.robot.shine(randomLight) + } + + func rotationDance() -> CGFloat { + let motions: [(duration: CGFloat, motion: Robot.Motion)] = [ + (3, .spin(.clockwise, speed: 1)), + (0.5, .stop), + (3, .spin(.counterclockwise, speed: 1)), + (0.5, .stop), + (0.5, .spin(.clockwise, speed: 1)), + (0.5, .spin(.counterclockwise, speed: 1)), + (0.5, .spin(.clockwise, speed: 1)), + (0.5, .spin(.counterclockwise, speed: 1)), + ] + self.lastMove += 1 + + let action = motions[lastMove % motions.count] + self.robot.move(action.motion) + + return action.duration + } + + func movementDance() -> CGFloat { + let motions: [(duration: CGFloat, motion: Robot.Motion)] = [ + (2, .forward(speed: 1)), (3, .spin(.clockwise, speed: 1)), + (2, .forward(speed: 1)), (3, .spin(.counterclockwise, speed: 1)), + ] + self.lastMove += 1 + + let action = motions[lastMove % motions.count] + self.robot.move(action.motion) + + return action.duration + } + + // MARK: Private + + private func getRandomColor() -> Robot.Color { + let colors: [Robot.Color] = [.red, .blue, .green, .yellow, .lightBlue, .purple, .orange, .pink] + return colors.randomElement()! + } + + private func getRandomLight(color: Robot.Color) -> Robot.Lights { + let lights: [Robot.Lights] = [ + .earLeft(in: color), .earRight(in: color), .quarterBackLeft(in: color), .quarterBackRight(in: color), + .quarterFrontLeft(in: color), .quarterFrontRight(in: color), + ] + return lights.randomElement()! + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+SongSelectorView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+SongSelectorView.swift new file mode 100644 index 0000000000..213612b373 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+SongSelectorView.swift @@ -0,0 +1,76 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import DesignKit +import LocalizationKit +import SwiftUI + +extension DanceFreezeView { + struct SongSelectorView: View { + // MARK: Lifecycle + + init(songs: [AudioRecording], selectedAudioRecording: Binding) { + self.songs = songs + self._selectedAudioRecording = selectedAudioRecording + } + + // MARK: Internal + + let columns = [ + GridItem(.flexible()), + GridItem(.flexible()), + ] + + @Binding var selectedAudioRecording: AudioRecording + let songs: [AudioRecording] + + var body: some View { + VStack(alignment: .leading) { + Text(l10n.DanceFreezeView.musicSelectionTitle) + .font(.headline) + + Divider() + + ScrollView { + LazyVGrid(columns: self.columns, alignment: .leading, spacing: 20) { + ForEach(self.songs, id: \.self) { audioRecording in + Label(audioRecording.name, systemImage: audioRecording == self.selectedAudioRecording + ? "checkmark.circle.fill" : "circle") + .imageScale(.large) + .foregroundColor( + audioRecording == self.selectedAudioRecording + ? .green : .primary + ) + .onTapGesture { + withAnimation { + self.selectedAudioRecording = audioRecording + } + } + } + } + } + .padding(.horizontal) + } + .padding(.vertical, 15) + .padding(.horizontal, 40) + } + } +} + +#Preview { + let songs = [ + AudioRecording(name: "Giggly Squirrel", file: "Giggly_Squirrel"), + AudioRecording(name: "Empty Page", file: "Empty_Page"), + AudioRecording(name: "Early Bird", file: "Early_Bird"), + AudioRecording(name: "Hands On", file: "Hands_On"), + AudioRecording(name: "In The Game", file: "In_The_Game"), + AudioRecording(name: "Little by Little", file: "Little_by_little"), + ] + + return DanceFreezeView.SongSelectorView( + songs: songs, + selectedAudioRecording: .constant(AudioRecording(.emptyPage)) + ) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+ViewModel.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+ViewModel.swift new file mode 100644 index 0000000000..a2df53f49a --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+ViewModel.swift @@ -0,0 +1,120 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import RobotKit +import SwiftUI + +extension DanceFreezeView { + class ViewModel: ObservableObject { + // MARK: Lifecycle + + init(selectedAudioRecording: AudioRecording, motion: Motion, shared: ExerciseSharedData? = nil) { + self.audioPlayer = AudioPlayer(audioRecording: selectedAudioRecording) + self.audioPlayer.setAudioPlayer(audioRecording: selectedAudioRecording) + self.robotManager = RobotManager() + self.motionMode = motion + + self.exercicesSharedData = shared ?? ExerciseSharedData() + self.exercicesSharedData.state = .playing + + self.subscribeToAudioPlayerProgress() + } + + // MARK: Public + + @Published public var progress: CGFloat = 0.0 + @Published public var isDancing: Bool = false + + public func onDanceFreezeToggle() { + guard self.progress < 1.0 else { + self.completeDanceFreeze() + return + } + + if self.audioPlayer.isPlaying { + self.audioPlayer.pause() + self.isDancing = false + self.robotManager.freeze() + } else { + self.audioPlayer.play() + self.isDancing = true + self.robotDance() + } + } + + // MARK: Internal + + @ObservedObject var exercicesSharedData: ExerciseSharedData + + func completeDanceFreeze() { + self.isDancing = false + self.exercicesSharedData.state = .completed(level: .nonApplicable) + self.robotManager.stopRobot() + self.audioPlayer.stop() + } + + // MARK: Private + + private var robotManager: RobotManager + private var audioPlayer: AudioPlayer + private var motionMode: Motion = .rotation + private var cancellables: Set = [] + + private func subscribeToAudioPlayerProgress() { + self.audioPlayer.$progress + .receive(on: DispatchQueue.main) + .sink { [weak self] in + guard let self else { return } + self.progress = $0 + if self.progress == 1 { + self.completeDanceFreeze() + } + } + .store(in: &self.cancellables) + } + + private func robotDance() { + switch self.motionMode { + case .rotation: + self.robotRotation() + case .movement: + self.robotMovement() + } + + self.robotLightFrenzy() + } + + private func robotLightFrenzy() { + guard self.isDancing, !self.exercicesSharedData.isCompleted else { return } + + self.robotManager.shineRandomly() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + self.robotLightFrenzy() + } + } + + private func robotRotation() { + guard self.isDancing, !self.exercicesSharedData.isCompleted else { return } + + let duration = self.robotManager.rotationDance() + + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { + self.robotRotation() + } + } + + private func robotMovement() { + guard self.isDancing, !self.exercicesSharedData.isCompleted else { return } + + let duration = self.robotManager.movementDance() + + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { + self.robotMovement() + } + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+l10n.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+l10n.swift new file mode 100644 index 0000000000..319a2886a0 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView+l10n.swift @@ -0,0 +1,39 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +extension l10n { + enum DanceFreezeView { + static let instructions = LocalizedString("lekaapp.dance_freeze_view.instructions", + bundle: GameEngineKitResources.bundle, + value: "Dance with Leka to the music and act like a statue when he stops", + comment: "DanceFreezeView instructions") + + static let musicSelectionTitle = LocalizedString("lekaapp.dance_freeze_view.music_selection_title", + bundle: GameEngineKitResources.bundle, + value: "Music selection", + comment: "DanceFreezeView music selection title") + + static let rotationButtonLabel = LocalizedString("lekaapp.dance_freeze_view.rotation_button_label", + bundle: GameEngineKitResources.bundle, + value: "Rotation", + comment: "DanceFreezeView rotation button label") + + static let movementButtonLabel = LocalizedString("lekaapp.dance_freeze_view.movement_button_label", + bundle: GameEngineKitResources.bundle, + value: "Movement", + comment: "DanceFreezeView movement button label") + + static let manualButtonLabel = LocalizedString("lekaapp.dance_freeze_view.manual_button_label", + bundle: GameEngineKitResources.bundle, + value: "Play - Manual mode", + comment: "DanceFreezeView manual button label") + + static let autoButtonLabel = LocalizedString("lekaapp.dance_freeze_view.auto_button_label", + bundle: GameEngineKitResources.bundle, + value: "Play - Auto mode", + comment: "DanceFreezeView auto button label") + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView.swift new file mode 100644 index 0000000000..e9792fcca5 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/DanceFreeze/DanceFreezeView.swift @@ -0,0 +1,78 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import DesignKit +import SwiftUI + +public struct DanceFreezeView: View { + // MARK: Lifecycle + + public init(songs: [AudioRecording]) { + self.songs = songs + self.selectedAudioRecording = songs.first! + self.data = nil + } + + public init(exercise: Exercise, data: ExerciseSharedData? = nil) { + guard let payload = exercise.payload as? DanceFreeze.Payload else { + fatalError("Exercise payload is not DanceFreeze.Payload") + } + + self.songs = payload.songs + self.selectedAudioRecording = self.songs.first! + self.data = data + } + + // MARK: Public + + public var body: some View { + switch self.mode { + case .waitingForSelection: + LauncherView(mode: self.$mode, + motion: self.$motion, + selectedAudioRecording: self.$selectedAudioRecording, + songs: self.songs) + case .automaticMode: + PlayerView(selectedAudioRecording: self.selectedAudioRecording, isAuto: true, motion: self.motion) + case .manualMode: + PlayerView(selectedAudioRecording: self.selectedAudioRecording, isAuto: false, motion: self.motion) + } + } + + // MARK: Internal + + enum Stage { + case waitingForSelection + case automaticMode + case manualMode + } + + enum Motion { + case rotation + case movement + } + + // MARK: Private + + private let data: ExerciseSharedData? + private let songs: [AudioRecording] + + @State private var mode = Stage.waitingForSelection + @State private var motion: Motion = .rotation + @State private var selectedAudioRecording: AudioRecording +} + +#Preview { + let songs = [ + AudioRecording(name: "Giggly Squirrel", file: "Giggly_Squirrel"), + AudioRecording(name: "Empty Page", file: "Empty_Page"), + AudioRecording(name: "Early Bird", file: "Early_Bird"), + AudioRecording(name: "Hands On", file: "Hands_On"), + AudioRecording(name: "In The Game", file: "In_The_Game"), + AudioRecording(name: "Little by Little", file: "Little_by_little"), + ] + + return DanceFreezeView(songs: songs) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView+ActionButton.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView+ActionButton.swift new file mode 100644 index 0000000000..5f45d48fd5 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView+ActionButton.swift @@ -0,0 +1,55 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +// MARK: - Action + +extension DiscoverLekaView { + struct ActionButton: View { + // MARK: Lifecycle + + init(_ actionType: ActionType, text: String, hasStarted: Bool = false, action: @escaping () -> Void) { + self.actionType = actionType + self.text = text + self.hasStarted = hasStarted + self.action = action + } + + // MARK: Internal + + let actionType: ActionType + let text: String + let hasStarted: Bool + let action: () -> Void + + var body: some View { + VStack { + Button { + self.action() + } label: { + self.actionType.icon(self.hasStarted) + } + + .background( + Circle() + .fill(.white) + .shadow(color: .black.opacity(0.5), radius: 7, x: 0, y: 4) + ) + + Text(self.text) + .font(.body) + .foregroundColor(DesignKitAsset.Colors.lekaDarkGray.swiftUIColor) + .padding(.vertical, 10) + } + } + } +} + +#Preview { + DiscoverLekaView.ActionButton(.stop, text: "Stop", hasStarted: true) { + print("Button tapped !") + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView+ActionType.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView+ActionType.swift new file mode 100644 index 0000000000..45322bcc67 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView+ActionType.swift @@ -0,0 +1,39 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +extension DiscoverLekaView { + enum ActionType { + case start + case pause + case stop + + // MARK: Internal + + func icon(_ isPlaying: Bool) -> some View { + let view = switch self { + case .start: + Image(systemName: "play.circle.fill") + .foregroundStyle(DesignKitAsset.Colors.lekaSkyBlue.swiftUIColor) + .font(.system(size: 150)) + case .pause: + Image(systemName: "pause.circle.fill") + .foregroundStyle(DesignKitAsset.Colors.btnDarkBlue.swiftUIColor) + .font(.system(size: 150)) + case .stop: + Image(systemName: "stop.circle.fill") + .foregroundStyle( + isPlaying + ? DesignKitAsset.Colors.lekaOrange.swiftUIColor + : DesignKitAsset.Colors.lekaDarkGray.swiftUIColor + ) + .font(.system(size: 150)) + } + + return view + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView+Animation.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView+Animation.swift new file mode 100644 index 0000000000..04ff51d533 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView+Animation.swift @@ -0,0 +1,68 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import RobotKit +import SwiftUI + +extension DiscoverLekaView { + enum Animation: CaseIterable { + case quickMove + case spin + case headNod + case headShake + case reinforcer + case light + + // MARK: Internal + + func actions() -> [(TimeInterval, () -> Void)] { + switch self { + case .quickMove: + let rotation = [Robot.Motion.Rotation.counterclockwise, Robot.Motion.Rotation.clockwise] + return [ + (0.3, { Robot.shared.move(.spin(rotation.randomElement()!, speed: 0.6)) }), + (0.0, { Robot.shared.stopMotion() }), + ] + case .spin: + let rotation = [Robot.Motion.Rotation.counterclockwise, Robot.Motion.Rotation.clockwise] + return [ + (2.2, { Robot.shared.move(.spin(rotation.randomElement()!, speed: 0.6)) }), + (0.0, { Robot.shared.stopMotion() }), + ] + case .headNod: + return [ + (0.2, { Robot.shared.move(.forward(speed: 0.3)) }), + (0.3, { Robot.shared.move(.backward(speed: 0.3)) }), + (0.2, { Robot.shared.move(.forward(speed: 0.3)) }), + (0.0, { Robot.shared.stopMotion() }), + ] + case .headShake: + return [ + (0.1, { Robot.shared.move(.spin(.clockwise, speed: 0.5)) }), + (0.15, { Robot.shared.move(.spin(.counterclockwise, speed: 0.5)) }), + (0.1, { Robot.shared.move(.spin(.clockwise, speed: 0.5)) }), + (0.1, { Robot.shared.move(.spin(.counterclockwise, speed: 0.5)) }), + (0.0, { Robot.shared.stopMotion() }), + ] + case .reinforcer: + let reinforcers: [Robot.Reinforcer] = [.fire, .rainbow, .sprinkles] + return [ + (5.0, { Robot.shared.run(reinforcers.randomElement()!) }), + ] + case .light: + let color: [Robot.Color] = [.blue, .green, .orange, .pink, .purple, .red, .yellow] + return [ + (3.0, { Robot.shared.shine(.all(in: color.randomElement()!)) }), + (0.1, { Robot.shared.stopLights() }), + ] + } + } + + func duration() -> TimeInterval { + self.actions().reduce(0) { total, action in + total + action.0 + } + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView+RobotManager.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView+RobotManager.swift new file mode 100644 index 0000000000..9ef83f451a --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView+RobotManager.swift @@ -0,0 +1,105 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import RobotKit +import SwiftUI + +extension DiscoverLekaView { + class RobotManager { + // MARK: Lifecycle + + init(data: ExerciseSharedData) { + self.shared = data + } + + // MARK: Internal + + let shared: ExerciseSharedData + + func startPairing() { + self.isAnimationRunning = true + self.runRandomAnimation() + } + + func pausePairing() { + self.isAnimationRunning = false + self.robot.stopMotion() + } + + func stopPairing() { + self.isAnimationRunning = false + self.robot.stop() + self.lightIntensity = 0.0 + } + + // MARK: Private + + private var isAnimationRunning: Bool = false + private var isBreathing: Bool = false + private var breatheIn = true + private var lightIntensity: Float = 0.0 + private var animationTime: TimeInterval = 0.0 + private let lightIntensityChangeDuration = 0.05 + private let robot = Robot.shared + + private func runRandomAnimation() { + guard self.isAnimationRunning, !self.shared.isCompleted else { return } + + let randomInterval = Double.random(in: 10.0...15.0) + self.isBreathing = true + self.breathe() + + DispatchQueue.main.asyncAfter(deadline: .now() + randomInterval) { + guard self.isAnimationRunning, !self.shared.isCompleted else { return } + self.isBreathing = false + let currentAnimation = Animation.allCases.randomElement()! + self.play(currentAnimation) + + DispatchQueue.main.asyncAfter(deadline: .now() + currentAnimation.duration()) { + guard self.isAnimationRunning, !self.shared.isCompleted else { return } + self.runRandomAnimation() + } + } + } + + private func play(_ animation: Animation) { + let actions = animation.actions() + self.animationTime = 0.1 + + for (duration, action) in actions { + DispatchQueue.main.asyncAfter(deadline: .now() + self.animationTime) { + guard self.isAnimationRunning, !self.shared.isCompleted else { return } + action() + } + self.animationTime += duration + } + } + + private func breathe() { + guard self.isAnimationRunning, self.isBreathing, !self.shared.isCompleted else { return } + + self.updateLightIntensity() + + DispatchQueue.main.asyncAfter(deadline: .now() + self.lightIntensityChangeDuration) { + guard self.isAnimationRunning, self.isBreathing, !self.shared.isCompleted else { return } + + let shadeOfColor: Robot.Color = .init(fromGradient: (.black, .lightBlue), at: self.lightIntensity) + self.robot.shine(.all(in: shadeOfColor)) + self.breathe() + } + } + + private func updateLightIntensity() { + if self.lightIntensity >= 1.0 { + self.breatheIn = false + } else if self.lightIntensity <= 0.0 { + self.breatheIn = true + } + + self.lightIntensity += self.breatheIn ? 0.02 : -0.02 + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView+l10n.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView+l10n.swift new file mode 100644 index 0000000000..a59ac672b6 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView+l10n.swift @@ -0,0 +1,34 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +extension l10n { + enum DiscoverLekaView { + static let instructions = LocalizedString("game_engine_kit.discover_leka_view.player.instructions", + bundle: GameEngineKitResources.bundle, + value: """ + "Discover Leka" allows the accompanied person to become familiar with Leka + before even entering into learning Activities and Curriculums. + The robot will come to life, taking pauses so that the care receiver can + tame your new companion! + """, + comment: "DiscoverLekaView instructions") + + static let playButtonLabel = LocalizedString("game_engine_kit.discover_leka_view.play_button_label", + bundle: GameEngineKitResources.bundle, + value: "Play", + comment: "Discover LekaView play button label") + + static let pauseButtonLabel = LocalizedString("game_engine_kit.discover_leka_view.pause_button_label", + bundle: GameEngineKitResources.bundle, + value: "Pause", + comment: "DiscoverLekaView pause button label") + + static let stopButtonLabel = LocalizedString("game_engine_kit.discover_leka_view.stop_button_label", + bundle: GameEngineKitResources.bundle, + value: "Stop", + comment: "DiscoverLekaView stop button label") + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView.swift new file mode 100644 index 0000000000..d1d2ecf85e --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/DiscoverLeka/DiscoverLekaView.swift @@ -0,0 +1,79 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import LocalizationKit +import SwiftUI + +// MARK: - DiscoverLekaView + +struct DiscoverLekaView: View { + // MARK: Lifecycle + + init() { + self.shared = ExerciseSharedData() + self.robotManager = RobotManager(data: ExerciseSharedData()) + } + + init(data: ExerciseSharedData? = nil) { + self.shared = data + self.robotManager = RobotManager(data: data!) + } + + // MARK: Internal + + let robotManager: RobotManager + let shared: ExerciseSharedData? + + var body: some View { + VStack { + Text(l10n.DiscoverLekaView.instructions) + .font(.headline) + .foregroundColor(.primary) + .multilineTextAlignment(.center) + + Spacer() + + HStack(spacing: 180) { + if self.isPlaying { + ActionButton(.pause, text: String(l10n.DiscoverLekaView.pauseButtonLabel.characters)) { + self.robotManager.pausePairing() + self.isPlaying = false + } + } else { + ActionButton(.start, text: String(l10n.DiscoverLekaView.playButtonLabel.characters)) { + self.robotManager.startPairing() + self.isPlaying = true + self.hasStarted = true + } + } + + ActionButton(.stop, text: String(l10n.DiscoverLekaView.stopButtonLabel.characters), hasStarted: self.hasStarted) { + self.robotManager.stopPairing() + + self.isPlaying = false + self.hasStarted = false + } + .disabled(!self.hasStarted) + } + + Spacer() + } + .onDisappear { + self.shared?.state = .completed(level: .nonApplicable) + self.robotManager.stopPairing() + self.isPlaying = false + self.hasStarted = false + } + } + + // MARK: Private + + @State private var isPlaying: Bool = false + @State private var hasStarted: Bool = false +} + +#Preview { + DiscoverLekaView() +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeek+l10n.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeek+l10n.swift new file mode 100644 index 0000000000..b87370eb07 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeek+l10n.swift @@ -0,0 +1,42 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +// swiftlint:disable nesting + +extension l10n { + enum HideAndSeekView { + enum Launcher { + static let instructions = LocalizedString("lekaapp.hide_and_seek_view.launcher.instructions", + bundle: GameEngineKitResources.bundle, + value: "Press OK when Leka is hidden", + comment: "HideAndSeekView Launcher instructions") + + static let okButtonLabel = LocalizedString("lekaapp.hide_and_seek_view.launcher.ok_button_label", + bundle: GameEngineKitResources.bundle, + value: "Ok", + comment: "HideAndSeekView Laucher OK button label") + } + + enum Player { + static let instructions = LocalizedString("lekaapp.hide_and_seek_view.player.instructions", + bundle: GameEngineKitResources.bundle, + value: """ + Encourage the care receiver to seek Leka. + You can throw a reinforcer to give him + a visual and/or audible clue. + Press FOUND! once the robot found. + """, + comment: "HideAndSeekView Player instructions") + + static let foundButtonLabel = LocalizedString("lekaapp.hide_and_seek_view.player.found_button_label", + bundle: GameEngineKitResources.bundle, + value: "Found!", + comment: "HideAndSeekView Player Found Button label") + } + } +} + +// swiftlint:enable nesting diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView+ButtonLabel.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView+ButtonLabel.swift new file mode 100644 index 0000000000..1d881004b1 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView+ButtonLabel.swift @@ -0,0 +1,32 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +extension HideAndSeekView { + struct ButtonLabel: View { + // MARK: Lifecycle + + init(_ text: String, color: Color) { + self.text = text + self.color = color + } + + // MARK: Internal + + let text: String + let color: Color + + var body: some View { + Text(self.text) + .font(.body) + .foregroundColor(.white) + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.center) + .frame(width: 400, height: 50) + .scaledToFit() + .background(Capsule().fill(self.color).shadow(radius: 3)) + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView+HiddenView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView+HiddenView.swift new file mode 100644 index 0000000000..0cd756d1b5 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView+HiddenView.swift @@ -0,0 +1,37 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import Lottie +import SwiftUI + +extension HideAndSeekView { + struct HiddenView: View { + // MARK: Internal + + var body: some View { + VStack { + Text(l10n.HideAndSeekView.Player.instructions) + .font(.headline) + .foregroundColor(.white) + .multilineTextAlignment(.center) + .padding(.top, 30) + + LottieView(animation: self.animation, speed: 0.5) + } + .background(.black) + .clipShape(RoundedRectangle(cornerRadius: 20)) + .padding(10) + } + + // MARK: Private + + private let animation = LottieAnimation.named("hide_and_seek_hidden.animation.lottie", bundle: .module)! + } +} + +#Preview { + HideAndSeekView.HiddenView() +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView+Launcher.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView+Launcher.swift new file mode 100644 index 0000000000..76d4bc30a1 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView+Launcher.swift @@ -0,0 +1,35 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import SwiftUI + +extension HideAndSeekView { + struct Launcher: View { + @Binding var stage: HideAndSeekStage + + var body: some View { + VStack { + Text(l10n.HideAndSeekView.Launcher.instructions) + .font(.headline) + GameEngineKitAsset.Exercises.HideAndSeek.imageIllustration.swiftUIImage + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 500, height: 500) + + Button { + self.stage = .hidden + } label: { + ButtonLabel(String(l10n.HideAndSeekView.Launcher.okButtonLabel.characters).uppercased(), color: .cyan) + } + } + .scaledToFill() + } + } +} + +#Preview { + HideAndSeekView.Launcher(stage: .constant(.toHide)) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView+Player.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView+Player.swift new file mode 100644 index 0000000000..8a40ae8f31 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView+Player.swift @@ -0,0 +1,97 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit +import RobotKit +import SwiftUI + +// swiftlint:disable nesting +extension HideAndSeekView { + struct Player: View { + // MARK: Lifecycle + + init(stage: Binding, shared: ExerciseSharedData? = nil) { + _stage = stage + + self.exercicesSharedData = shared ?? ExerciseSharedData() + self.exercicesSharedData.state = .playing + } + + // MARK: Internal + + enum Stimulation: String, CaseIterable { + case light + case motion + + // MARK: Public + + public func icon() -> Image { + switch self { + case .light: + GameEngineKitAsset.Exercises.HideAndSeek.iconStimulationLight.swiftUIImage + case .motion: + GameEngineKitAsset.Exercises.HideAndSeek.iconStimulationMotion.swiftUIImage + } + } + } + + @Binding var stage: HideAndSeekStage + @ObservedObject var exercicesSharedData: ExerciseSharedData + let robotManager = RobotManager() + + var body: some View { + ZStack { + HiddenView() + .padding(.horizontal, 30) + + HStack { + Spacer() + VStack(spacing: 70) { + self.stimulationButton(Stimulation.light) { + self.robotManager.runRandomReinforcer() + } + self.stimulationButton(Stimulation.motion) { + self.robotManager.wiggle(for: 1) + } + } + .padding(.trailing, 60) + } + + VStack { + Spacer() + + Button { + self.exercicesSharedData.state = .completed(level: .nonApplicable) + } label: { + ButtonLabel(String(l10n.HideAndSeekView.Player.foundButtonLabel.characters).uppercased(), color: .cyan) + } + .padding(.vertical, 30) + } + } + .padding(.vertical, 40) + } + + func stimulationButton(_ stimulation: Stimulation, action: @escaping (() -> Void)) -> some View { + stimulation.icon() + .resizable() + .renderingMode(.original) + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 108, maxHeight: 108) + .padding(20) + .background( + Circle() + .fill(.white) + ) + .onTapGesture { + action() + } + } + } +} + +// swiftlint:enable nesting + +#Preview { + HideAndSeekView.Player(stage: .constant(.hidden), shared: ExerciseSharedData()) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView+RobotManager.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView+RobotManager.swift new file mode 100644 index 0000000000..7a52874e27 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView+RobotManager.swift @@ -0,0 +1,39 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import RobotKit +import SwiftUI + +extension HideAndSeekView { + class RobotManager { + let reinforcers: [Robot.Reinforcer] = [.fire, .rainbow, .sprinkles] + let robot = Robot.shared + + func wiggle(for duration: CGFloat) { + guard duration > 0 else { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + self.robot.stopMotion() + } + return + } + log.trace("🤖 WIGGLE for \(duration) seconds") + let motionDuration = 0.2 + + DispatchQueue.main.asyncAfter(deadline: .now() + motionDuration) { + self.robot.move(.spin(.clockwise, speed: 1)) + + DispatchQueue.main.asyncAfter(deadline: .now() + motionDuration) { + self.robot.move(.spin(.counterclockwise, speed: 1)) + self.wiggle(for: duration - motionDuration * 2) + } + } + } + + func runRandomReinforcer() { + self.robot.run(self.reinforcers.randomElement()!) + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView.swift new file mode 100644 index 0000000000..27a60ad9eb --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/HideAndSeek/HideAndSeekView.swift @@ -0,0 +1,44 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +struct HideAndSeekView: View { + // MARK: Lifecycle + + public init() { + self.stage = .toHide + self.shared = ExerciseSharedData() + } + + init(exercise _: Exercise, data: ExerciseSharedData? = nil) { + self.stage = .toHide + self.shared = data + } + + // MARK: Public + + public var body: some View { + switch self.stage { + case .toHide: + Launcher(stage: self.$stage) + case .hidden: + Player(stage: self.$stage, shared: self.shared) + } + } + + // MARK: Internal + + enum HideAndSeekStage { + case toHide + case hidden + } + + let shared: ExerciseSharedData? + + // MARK: Private + + @State private var stage: HideAndSeekStage +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/Instrument/MusicalInstrumentView+XylophoneView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/Instrument/MusicalInstrumentView+XylophoneView.swift new file mode 100644 index 0000000000..a0bd334428 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/Instrument/MusicalInstrumentView+XylophoneView.swift @@ -0,0 +1,54 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AudioKit +import RobotKit +import SwiftUI + +extension MusicalInstrumentView { + struct XylophoneView: View { + // MARK: Lifecycle + + init(midiPlayer: MIDIPlayer, scale: MIDIScale) { + self.xyloPlayer = midiPlayer + self.scale = scale + self.tileNumber = scale.notes.count + self.tilesSpacing = scale.self == .majorPentatonic ? 40 : 20 + } + + // MARK: Internal + + @ObservedObject var xyloPlayer: MIDIPlayer + let tilesSpacing: CGFloat + let tileNumber: Int + let tileColors: [Robot.Color] = [.pink, .red, .orange, .yellow, .green, .lightBlue, .blue, .purple] + let scale: MIDIScale + + var body: some View { + HStack(spacing: self.tilesSpacing) { + ForEach(0..) { + self._keyboard = keyboard + } + + // MARK: Internal + + var body: some View { + HStack(spacing: 40) { + VStack(spacing: 0) { + GameEngineKitAsset.Exercises.Melody.iconKeyboardPartial.swiftUIImage + .resizable() + .scaledToFit() + Text(l10n.MelodyView.partialKeyboardLabel) + .foregroundStyle(self.keyboard == .partial ? .black : .gray.opacity(0.4)) + } + .onTapGesture { + withAnimation { + self.keyboard = .partial + } + } + + VStack(spacing: 0) { + GameEngineKitAsset.Exercises.Melody.iconKeyboardFull.swiftUIImage + .resizable() + .scaledToFit() + Text(l10n.MelodyView.fullKeyboardLabel) + .foregroundStyle(self.keyboard == .full ? .black : .gray.opacity(0.4)) + } + .onTapGesture { + withAnimation { + self.keyboard = .full + } + } + } + .padding(.vertical, 15) + .padding(.horizontal, 40) + } + + // MARK: Private + + @Binding private var keyboard: KeyboardType + } +} + +#Preview { + let songs = [ + MidiRecording(.aGreenMouse), + MidiRecording(.londonBridgeIsFallingDown), + MidiRecording(.twinkleTwinkleLittleStar), + MidiRecording(.underTheMoonlight), + ] + + return MelodyView.KeyboardModeView( + keyboard: .constant(.partial) + ) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+LauncherView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+LauncherView.swift new file mode 100644 index 0000000000..197d8bedf9 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+LauncherView.swift @@ -0,0 +1,58 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import LocalizationKit +import SwiftUI + +extension MelodyView { + struct LauncherView: View { + @Binding var selectedSong: MidiRecording + @Binding var mode: Stage + @Binding var keyboard: KeyboardType + let songs: [MidiRecording] + + var body: some View { + VStack(spacing: 100) { + HStack(spacing: 30) { + GameEngineKitAsset.Exercises.Melody.imageIllustration.swiftUIImage + .resizable() + .aspectRatio(contentMode: .fit) + .padding(.trailing, 50) + + VStack(spacing: 0) { + KeyboardModeView(keyboard: self.$keyboard) + + SongSelectorView(songs: self.songs, selectedMidiRecording: self.$selectedSong) + } + } + .padding(.horizontal, 100) + + Button { + self.mode = .selectionConfirmed + } label: { + ButtonLabel(String(l10n.MelodyView.playButtonLabel.characters), color: .cyan) + } + } + } + } +} + +#Preview { + let songs = [ + MidiRecording(.underTheMoonlight), + MidiRecording(.aGreenMouse), + MidiRecording(.twinkleTwinkleLittleStar), + MidiRecording(.londonBridgeIsFallingDown), + MidiRecording(.ohTheCrocodiles), + MidiRecording(.happyBirthday), + ] + + return MelodyView.LauncherView( + selectedSong: .constant(MidiRecording(.underTheMoonlight)), + mode: .constant(.waitingForSelection), + keyboard: .constant(.partial), + songs: songs + ) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+PlayerButton.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+PlayerButton.swift new file mode 100644 index 0000000000..af864e4c2e --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+PlayerButton.swift @@ -0,0 +1,47 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import DesignKit +import SwiftUI + +extension MelodyView { + struct PlayerButton: View { + // MARK: Internal + + @Binding var showModal: Bool + let action: () -> Void + + var body: some View { + VStack { + Button { + self.action() + withAnimation { + self.isMelodyPlaying.toggle() + } + } label: { + Image(systemName: self.isMelodyPlaying ? "speaker.wave.2.circle" : "play.circle.fill") + .resizable() + .scaledToFit() + .background { + Circle() + .fill(.white) + } + } + .disabled(self.isMelodyPlaying) + .frame(width: 300) + } + } + + // MARK: Private + + @State private var isMelodyPlaying: Bool = false + } +} + +#Preview { + MelodyView.PlayerButton(showModal: .constant(true)) { + print("Play !") + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+SongSelectorView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+SongSelectorView.swift new file mode 100644 index 0000000000..11243e4162 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+SongSelectorView.swift @@ -0,0 +1,74 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import DesignKit +import LocalizationKit +import SwiftUI + +extension MelodyView { + struct SongSelectorView: View { + // MARK: Lifecycle + + init(songs: [MidiRecording], selectedMidiRecording: Binding) { + self.songs = songs + self._selectedMidiRecording = selectedMidiRecording + } + + // MARK: Internal + + @Binding var selectedMidiRecording: MidiRecording + let songs: [MidiRecording] + + let columns = [ + GridItem(.flexible()), + GridItem(.flexible()), + ] + + var body: some View { + VStack(alignment: .leading) { + Text(l10n.MelodyView.musicSelectionTitle) + .font(.headline) + + Divider() + + ScrollView { + LazyVGrid(columns: self.columns, alignment: .leading, spacing: 20) { + ForEach(self.songs, id: \.self) { midiRecording in + Label(midiRecording.name, systemImage: midiRecording == self.selectedMidiRecording + ? "checkmark.circle.fill" : "circle") + .imageScale(.large) + .foregroundColor( + midiRecording == self.selectedMidiRecording + ? .green : .primary + ) + .onTapGesture { + withAnimation { + self.selectedMidiRecording = midiRecording + } + } + } + } + } + .padding(.horizontal) + } + .padding(.vertical, 15) + .padding(.horizontal, 40) + } + } +} + +#Preview { + let songs = [ + MidiRecording(.aGreenMouse), + MidiRecording(.londonBridgeIsFallingDown), + MidiRecording(.twinkleTwinkleLittleStar), + MidiRecording(.underTheMoonlight), + ] + + return MelodyView.SongSelectorView( + songs: songs, + selectedMidiRecording: .constant(MidiRecording(.underTheMoonlight)) + ) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+ViewModel.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+ViewModel.swift new file mode 100644 index 0000000000..d81e9d6281 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+ViewModel.swift @@ -0,0 +1,143 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AudioKit +import ContentKit +import RobotKit +import SwiftUI + +extension MelodyView { + class ViewModel: Identifiable, ObservableObject { + // MARK: Lifecycle + + init(midiPlayer: MIDIPlayer, selectedSong: MidiRecording, shared: ExerciseSharedData? = nil) { + self.midiPlayer = midiPlayer + self.defaultScale = selectedSong.scale + self.exercicesSharedData = shared ?? ExerciseSharedData() + self.exercicesSharedData.state = .playing + self.setMIDIRecording(midiRecording: selectedSong) + } + + // MARK: Public + + @ObservedObject public var exercicesSharedData: ExerciseSharedData + @Published public var progress: CGFloat = 0.0 + @Published public var isNotTappable: Bool = true + @Published public var showModal: Bool = false + public var midiPlayer: MIDIPlayer + public var scale: [MIDINoteNumber] = [] + public var currentNoteNumber: MIDINoteNumber = 0 + + public let defaultScale: [MIDINoteNumber] + public let tileColors: [Robot.Color] = [.pink, .red, .orange, .yellow, .green, .lightBlue, .blue, .purple] + + // MARK: Internal + + func setMIDIRecording(midiRecording: MidiRecording) { + let midiFile = Bundle.module.url(forResource: midiRecording.file, withExtension: "mid")! + self.midiPlayer.loadMIDIFile(fileURL: midiFile, tempo: self.tempo) + self.midiNotes = self.midiPlayer.getMidiNotes() + self.octaveGap = self.getOctaveGap(self.midiNotes.first!.noteNumber) + self.scale = self.getScale(self.midiNotes) + self.currentNoteNumber = self.midiNotes.first!.noteNumber - self.octaveGap + self.setInstrumentCallback() + } + + func playMIDIRecording() { + self.midiPlayer.play() + + DispatchQueue.main.asyncAfter(deadline: .now() + self.midiPlayer.getDuration()) { + self.robot.stopLights() + self.showModal = false + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + self.startActivity() + } + } + } + + func startActivity() { + self.isNotTappable = false + self.showColorFromMIDINote(self.currentNoteNumber) + } + + func onTileTapped(noteNumber: MIDINoteNumber) { + guard self.currentNoteIndex < self.midiNotes.count else { return } + + if noteNumber == self.currentNoteNumber { + self.isNotTappable = true + self.midiPlayer.noteOn( + number: self.currentNoteNumber, velocity: self.midiNotes[self.currentNoteIndex].velocity + ) + self.currentNoteIndex += 1 + self.robot.stopLights() + if self.currentNoteIndex < self.midiNotes.count { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + self.currentNoteNumber = self.midiNotes[self.currentNoteIndex].noteNumber - self.octaveGap + self.showColorFromMIDINote(self.currentNoteNumber) + self.isNotTappable = false + } + } else { + self.robot.stopLights() + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + self.midiPlayer.play() + + DispatchQueue.main.asyncAfter(deadline: .now() + self.midiPlayer.getDuration()) { + self.exercicesSharedData.state = .completed(level: .nonApplicable) + self.robot.stopLights() + self.midiPlayer.stop() + } + } + } + self.progress = CGFloat(self.currentNoteIndex) / CGFloat(self.midiNotes.count) + } + } + + func showColorFromMIDINote(_ note: MIDINoteNumber) { + guard let index = defaultScale.firstIndex(where: { + $0 == note + }) + else { + fatalError("Note not found") + } + self.robot.shine(.all(in: self.tileColors[index])) + } + + // MARK: Private + + private let tempo: Double = 100 + private let robot = Robot.shared + private let defaultOctave: UInt8 = 2 + private var midiNotes: [MIDINoteData] = [] + private var currentNoteIndex: Int = 0 + private var octaveGap: MIDINoteNumber = 0 + + private func setInstrumentCallback() { + self.midiPlayer.setInstrumentCallback(callback: { _, note, velocity in + if velocity == 0 || note < self.octaveGap { return } + let currentNote = note - self.octaveGap + if currentNote >= 24, currentNote <= 36 { + self.showColorFromMIDINote(currentNote) + self.midiPlayer.noteOn(number: currentNote, velocity: velocity) + } + }) + } + + private func getOctaveGap(_ initialNote: MIDINoteNumber) -> MIDINoteNumber { + let initialOctave = initialNote / 12 + return (initialOctave - self.defaultOctave) * 12 + } + + private func getScale(_ notes: [MIDINoteData]) -> [MIDINoteNumber] { + var uniqueDict: [MIDINoteNumber: MIDINoteData] = [:] + + for note in notes { + uniqueDict[note.noteNumber - self.octaveGap] = note + } + + return Array(uniqueDict.keys).sorted() + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+XylophoneView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+XylophoneView.swift new file mode 100644 index 0000000000..2e9ac6276d --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+XylophoneView.swift @@ -0,0 +1,126 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AudioKit +import ContentKit +import RobotKit +import SwiftUI + +// swiftlint:disable nesting +public extension MelodyView { + struct XylophoneView: View { + // MARK: Lifecycle + + init( + instrument: MIDIInstrument, selectedSong: MidiRecording, keyboard: KeyboardType, data: ExerciseSharedData? = nil + ) { + self._viewModel = StateObject( + wrappedValue: ViewModel( + midiPlayer: MIDIPlayer(instrument: instrument), selectedSong: selectedSong, shared: data + )) + self.keyboard = keyboard + self.scale = selectedSong.scale + } + + // MARK: Public + + public var body: some View { + ZStack { + VStack(spacing: 50) { + ContinuousProgressBar(progress: self.viewModel.progress) + .animation(.easeOut, value: self.viewModel.progress) + .padding(.horizontal) + + HStack(spacing: self.tilesSpacing) { + ForEach(self.scale.enumerated().map { $0 }, id: \.0) { index, note in + Button { + self.viewModel.onTileTapped(noteNumber: note) + } label: { + self.viewModel.tileColors[index].screen + } + .buttonStyle( + XylophoneTileButtonStyle( + index: index, + tileNumber: self.scale.count, + tileWidth: 100, + isTappable: self.viewModel.scale.contains(note) + ) + ) + .disabled(self.viewModel.isNotTappable) + .modifier( + KeyboardModeModifier( + isPartial: self.keyboard == .partial, isDisabled: !self.viewModel.scale.contains(note) + ) + ) + .compositingGroup() + } + } + } + .blur(radius: self.viewModel.showModal ? 10 : 0) + + if self.viewModel.showModal { + Color.clear + .frame(maxWidth: .infinity, maxHeight: .infinity) + .contentShape(Rectangle()) + .onTapGesture { + self.viewModel.showModal = false + self.viewModel.midiPlayer.stop() + self.viewModel.startActivity() + } + + PlayerButton(showModal: self.$viewModel.showModal) { + self.viewModel.playMIDIRecording() + } + } + } + .onAppear { + self.viewModel.showModal = true + } + .onDisappear { + self.viewModel.setMIDIRecording( + midiRecording: MidiRecording(.none)) + self.viewModel.midiPlayer.stop() + } + } + + // MARK: Internal + + struct KeyboardModeModifier: ViewModifier { + @Environment(\.colorScheme) var colorScheme + var isPartial: Bool + var isDisabled: Bool + + func body(content: Content) -> some View { + if self.isPartial { + content + .disabled(self.isDisabled) + .overlay { + if self.isDisabled { + RoundedRectangle(cornerRadius: 5) + .fill(self.colorScheme == .light ? Color.white.opacity(0.9) : Color.black.opacity(0.85)) + } + } + } else { + content + } + } + } + + // MARK: Private + + @StateObject private var viewModel: ViewModel + + private var scale: [MIDINoteNumber] + private let tilesSpacing: CGFloat = 16 + private let keyboard: KeyboardType + } +} + +// swiftlint:enable nesting + +#Preview { + MelodyView.XylophoneView( + instrument: .xylophone, selectedSong: MidiRecording(.aGreenMouse), keyboard: .full + ) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+l10n.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+l10n.swift new file mode 100644 index 0000000000..79ac8f6e7d --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView+l10n.swift @@ -0,0 +1,29 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +extension l10n { + enum MelodyView { + static let musicSelectionTitle = LocalizedString("lekaapp.melody_view.song_selector_title", + bundle: GameEngineKitResources.bundle, + value: "Music selection", + comment: "MelodyView music selection title") + + static let playButtonLabel = LocalizedString("lekaapp.melody_view.play_button_label", + bundle: GameEngineKitResources.bundle, + value: "Play", + comment: "MelodyView play button label") + + static let partialKeyboardLabel = LocalizedString("lekaapp.melody_view.keyboard_partial", + bundle: GameEngineKitResources.bundle, + value: "Partial keyboard", + comment: "MelodyView partial keyboard label") + + static let fullKeyboardLabel = LocalizedString("lekaapp.melody_view.keyboard_full", + bundle: GameEngineKitResources.bundle, + value: "Full keyboard", + comment: "MelodyView keyboard full keyboard label") + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView.swift new file mode 100644 index 0000000000..c309e5e4ce --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/Melody/MelodyView.swift @@ -0,0 +1,81 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AudioKit +import ContentKit +import SwiftUI + +public struct MelodyView: View { + // MARK: Lifecycle + + init(instrument: MIDIInstrument, songs: [MidiRecording]) { + self.instrument = instrument + self.songs = songs + self.selectedSong = songs.first! + self.data = nil + } + + init(exercise: Exercise, data: ExerciseSharedData? = nil) { + guard let payload = exercise.payload as? MidiRecordingPlayer.Payload else { + fatalError("Exercise payload is not .instrument") + } + + guard let instrument = MIDIInstrument(rawValue: payload.instrument) + else { + fatalError("Instrument or song not found") + } + + self.instrument = instrument + self.songs = payload.songs + self.selectedSong = self.songs.first! + self.data = data + } + + // MARK: Public + + public var body: some View { + NavigationStack { + switch self.mode { + case .waitingForSelection: + LauncherView( + selectedSong: self.$selectedSong, mode: self.$mode, keyboard: self.$keyboard, songs: self.songs + ) + case .selectionConfirmed: + switch self.instrument { + case .xylophone: + XylophoneView( + instrument: self.instrument, selectedSong: self.selectedSong, keyboard: self.keyboard, data: self.data + ) + } + } + } + .navigationViewStyle(StackNavigationViewStyle()) + } + + // MARK: Internal + + enum Stage { + case waitingForSelection + case selectionConfirmed + } + + enum KeyboardType { + case full + case partial + } + + let data: ExerciseSharedData? + let instrument: MIDIInstrument + let songs: [MidiRecording] + + // MARK: Private + + @State private var mode = Stage.waitingForSelection + @State private var selectedSong: MidiRecording + @State private var keyboard: KeyboardType = .partial +} + +#Preview { + MelodyView(instrument: .xylophone, songs: [MidiRecording(.underTheMoonlight)]) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteArrow/RemoteArrowView+ArrowButton.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteArrow/RemoteArrowView+ArrowButton.swift new file mode 100644 index 0000000000..ed83de341c --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteArrow/RemoteArrowView+ArrowButton.swift @@ -0,0 +1,88 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import RobotKit +import SwiftUI + +// swiftlint:disable identifier_name nesting + +extension RemoteArrowView { + struct ArrowButton: View { + enum Arrow { + case up + case clockwise + case down + case counterclockwise + + // MARK: Internal + + var name: String { + switch self { + case .up: + "arrow.up" + case .clockwise: + "arrow.clockwise" + case .down: + "arrow.down" + case .counterclockwise: + "arrow.counterclockwise" + } + } + + var color: Robot.Color { + switch self { + case .up: + .blue + case .clockwise: + .red + case .down: + .green + case .counterclockwise: + .yellow + } + } + } + + @State private var isPressed = false + + let arrow: Arrow + let onChanged: () -> Void + let onReleased: () -> Void + + var body: some View { + Circle() + .fill(.white) + .frame(width: 200, height: 200) + .overlay { + Image(systemName: self.arrow.name) + .resizable() + .foregroundColor(self.arrow.color.screen) + .frame(width: 80, height: 100) + } + .shadow(color: .black.opacity(0.1), radius: 10, x: 0, y: 0) + .scaleEffect(self.isPressed ? 0.95 : 1.0) + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { _ in + self.onChanged() + self.isPressed = true + } + .onEnded { _ in + self.onReleased() + self.isPressed = false + } + ) + } + } +} + +#Preview { + RemoteArrowView.ArrowButton(arrow: .counterclockwise) { + print("Button pressed") + } onReleased: { + print("Button released") + } +} + +// swiftlint:enable identifier_name nesting diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteArrow/RemoteArrowView+CircleLayout.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteArrow/RemoteArrowView+CircleLayout.swift new file mode 100644 index 0000000000..defe9b261a --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteArrow/RemoteArrowView+CircleLayout.swift @@ -0,0 +1,29 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +extension RemoteArrowView { + struct CircleLayout: Layout { + func sizeThatFits(proposal: ProposedViewSize, subviews _: Subviews, cache _: inout ()) -> CGSize { + proposal.replacingUnspecifiedDimensions() + } + + func placeSubviews(in bounds: CGRect, proposal _: ProposedViewSize, subviews: Subviews, cache _: inout ()) { + let angle = Angle.degrees(360 / Double(subviews.count)).radians + let posX = bounds.midX + let posY = bounds.midY + + for (index, subview) in subviews.enumerated() { + var point = CGPoint(x: 180, y: 0) + .applying(CGAffineTransform(rotationAngle: CGFloat(angle) * CGFloat(index - 1))) + + point.x += posX + point.y += posY + + subview.place(at: point, anchor: .center, proposal: .unspecified) + } + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteArrow/RemoteArrowView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteArrow/RemoteArrowView.swift new file mode 100644 index 0000000000..0d69ef1c8a --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteArrow/RemoteArrowView.swift @@ -0,0 +1,47 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import RobotKit +import SwiftUI + +struct RemoteArrowView: View { + let robot = Robot.shared + + var body: some View { + CircleLayout { + ArrowButton(arrow: .up) { + self.robot.move(.forward(speed: 1)) + self.robot.shine(.all(in: ArrowButton.Arrow.up.color)) + } onReleased: { + self.robot.stopMotion() + self.robot.stopLights() + } + ArrowButton(arrow: .clockwise) { + self.robot.move(.spin(.clockwise, speed: 1)) + self.robot.shine(.all(in: ArrowButton.Arrow.clockwise.color)) + } onReleased: { + self.robot.stopMotion() + self.robot.stopLights() + } + ArrowButton(arrow: .down) { + self.robot.move(.backward(speed: 1)) + self.robot.shine(.all(in: ArrowButton.Arrow.down.color)) + } onReleased: { + self.robot.stopMotion() + self.robot.stopLights() + } + ArrowButton(arrow: .counterclockwise) { + self.robot.move(.spin(.counterclockwise, speed: 1)) + self.robot.shine(.all(in: ArrowButton.Arrow.counterclockwise.color)) + } onReleased: { + self.robot.stopMotion() + self.robot.stopLights() + } + } + } +} + +#Preview { + RemoteArrowView() +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/Joystick/JoystickView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/Joystick/JoystickView.swift new file mode 100644 index 0000000000..e9fc6df431 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/Joystick/JoystickView.swift @@ -0,0 +1,59 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI +import SwiftUIJoystick + +struct JoystickView: View { + // MARK: Public + + public var body: some View { + VStack { + JoystickBuilder( + monitor: self.joystickViewViewModel.joystickMonitor, + width: self.joystickViewViewModel.dragDiameter, + shape: self.joystickViewViewModel.shape, + + background: { + ZStack { + Circle() + .fill(.white) + Circle() + .stroke(lineWidth: 2) + .fill(.gray) + + VStack(spacing: 110) { + Image(systemName: "arrow.up") + .foregroundColor(.gray.opacity(0.7)) + + HStack(spacing: 250) { + Image(systemName: "arrow.counterclockwise") + .foregroundColor(.gray.opacity(0.7)) + + Image(systemName: "arrow.clockwise") + .foregroundColor(.gray.opacity(0.7)) + } + + Image(systemName: "arrow.down") + .foregroundColor(.gray.opacity(0.7)) + } + } + }, + foreground: { + Circle() + .fill(.gray) + }, + locksInPlace: false + ) + } + } + + // MARK: Private + + @StateObject private var joystickViewViewModel = JoystickViewViewModel(dragDiameter: 300) +} + +#Preview { + JoystickView() +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/Joystick/JoystickViewViewModel.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/Joystick/JoystickViewViewModel.swift new file mode 100644 index 0000000000..23e8051a40 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/Joystick/JoystickViewViewModel.swift @@ -0,0 +1,40 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import RobotKit +import SwiftUI +import SwiftUIJoystick + +class JoystickViewViewModel: ObservableObject { + // MARK: Lifecycle + + init(dragDiameter: CGFloat) { + self.dragDiameter = dragDiameter + + self.joystickMonitor.$xyPoint + .receive(on: DispatchQueue.main) + .sink(receiveValue: { + self.position = $0 + + let (leftSpeed, rightSpeed) = convertJoystickPosToSpeed(position: $0, maxValue: dragDiameter) + + Robot.shared.move(.free(left: Float(leftSpeed), right: Float(rightSpeed))) + }) + .store(in: &self.cancellables) + } + + // MARK: Internal + + var joystickMonitor = JoystickMonitor() + + let dragDiameter: CGFloat + let shape: JoystickShape = .circle + + // MARK: Private + + @Published private var position: CGPoint = .init(x: 0.0, y: 0.0) + + private var cancellables: Set = [] +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+BeltSectionButton.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+BeltSectionButton.swift new file mode 100644 index 0000000000..e3396edd4f --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+BeltSectionButton.swift @@ -0,0 +1,78 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import RobotKit +import SwiftUI + +public extension Robot.Lights { + var arcAngle: (start: Angle, end: Angle) { + switch self { + case .full: + (start: .degrees(0), end: .degrees(360)) + case .halfRight: + (start: .degrees(10), end: .degrees(170)) + case .halfLeft: + (start: .degrees(190), end: .degrees(350)) + case .quarterFrontRight: + (start: .degrees(10), end: .degrees(80)) + case .quarterBackRight: + (start: .degrees(100), end: .degrees(170)) + case .quarterBackLeft: + (start: .degrees(190), end: .degrees(260)) + case .quarterFrontLeft: + (start: .degrees(280), end: .degrees(350)) + default: + (start: .degrees(0), end: .degrees(0)) + } + } +} + +// MARK: - LedZoneSelectorView.BeltSectionButton + +extension LedZoneSelectorView { + struct BeltSectionButton: View { + // MARK: Internal + + var section: Robot.Lights + let robot = Robot.shared + + var body: some View { + LedZoneShape(section: self.section) + .stroke(self.section.color.screen, style: StrokeStyle(lineWidth: 10, lineCap: .round)) + .frame(width: 300, height: 300) + .onTapGesture { + self.buttonPressed.toggle() + if self.buttonPressed { + self.robot.shine(self.section) + } else { + self.robot.blacken(self.section) + } + self.backgroundLineWidth = self.buttonPressed ? 25 : 0 + } + .background( + LedZoneShape(section: self.section) + .stroke( + self.section.color.screen.opacity(0.3), + style: StrokeStyle( + lineWidth: CGFloat(self.backgroundLineWidth), + lineCap: .round, + lineJoin: .round, + miterLimit: 10 + ) + ) + .frame(width: 300, height: 300) + ) + .animation(Animation.easeInOut(duration: 0.2), value: self.backgroundLineWidth) + } + + // MARK: Private + + @State private var buttonPressed = false + @State private var backgroundLineWidth = 0 + } +} + +#Preview { + LedZoneSelectorView.BeltSectionButton(section: .full(.belt, in: .red)) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+BeltSectionIcon.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+BeltSectionIcon.swift new file mode 100644 index 0000000000..4e217d9454 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+BeltSectionIcon.swift @@ -0,0 +1,28 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import RobotKit +import SwiftUI + +extension LedZoneSelectorView { + struct BeltSectionIcon: View { + // MARK: Internal + + var section: Robot.Lights + + var body: some View { + LedZoneShape(section: self.section) + .stroke(.black, style: StrokeStyle(lineWidth: 3, lineCap: .round)) + .frame(width: 60, height: 60) + } + + // MARK: Private + + @State private var backgroundLineWidth = 0 + } +} + +#Preview { + LedZoneSelectorView.BeltSectionIcon(section: .full(.belt, in: .red)) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+EarButton.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+EarButton.swift new file mode 100644 index 0000000000..3077f5ff7c --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+EarButton.swift @@ -0,0 +1,51 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import RobotKit +import SwiftUI + +extension LedZoneSelectorView { + struct EarButton: View { + // MARK: Lifecycle + + init(selectedEar: Robot.Lights) { + self.selectedEar = selectedEar + } + + // MARK: Internal + + let selectedEar: Robot.Lights + let robot = Robot.shared + + var body: some View { + Circle() + .foregroundColor(self.selectedEar.color.screen) + .frame(width: 50, height: 50) + .onTapGesture { + self.buttonPressed.toggle() + if self.buttonPressed { + self.robot.shine(self.selectedEar) + } else { + self.robot.blacken(self.selectedEar) + } + self.backgroundDimension = self.buttonPressed ? 65 : 0 + } + .background( + Circle() + .foregroundColor(self.selectedEar.color.screen.opacity(0.5)) + .frame(width: CGFloat(self.backgroundDimension), height: CGFloat(self.backgroundDimension)) + ) + .animation(.easeInOut(duration: 0.2), value: self.backgroundDimension) + } + + // MARK: Private + + @State private var buttonPressed = false + @State private var backgroundDimension = 0 + } +} + +#Preview { + LedZoneSelectorView.EarButton(selectedEar: .earRight(in: .blue)) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+EarIcon.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+EarIcon.swift new file mode 100644 index 0000000000..5cafe997dd --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+EarIcon.swift @@ -0,0 +1,19 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +extension LedZoneSelectorView { + struct EarIcon: View { + var body: some View { + Circle() + .foregroundColor(.black) + .frame(width: 10, height: 10) + } + } +} + +#Preview { + LedZoneSelectorView.EarIcon() +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+LedZoneShape.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+LedZoneShape.swift new file mode 100644 index 0000000000..f936e64a43 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+LedZoneShape.swift @@ -0,0 +1,31 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import RobotKit +import SwiftUI + +extension LedZoneSelectorView { + struct LedZoneShape: Shape { + let section: Robot.Lights + + func path(in rect: CGRect) -> Path { + let rotationAdjustment = Angle.degrees(90) + let modifiedStart = self.section.arcAngle.start - rotationAdjustment + let modifiedEnd = self.section.arcAngle.end - rotationAdjustment + + var path = Path() + + path.addArc( + center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.width / 2, startAngle: modifiedStart, + endAngle: modifiedEnd, clockwise: false + ) + + return path + } + } +} + +#Preview { + LedZoneSelectorView.LedZoneShape(section: .quarterBackLeft(in: .red)) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+ModeButton.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+ModeButton.swift new file mode 100644 index 0000000000..4ec77e40a4 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+ModeButton.swift @@ -0,0 +1,68 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import RobotKit +import SwiftUI + +extension LedZoneSelectorView { + struct ModeButton: View { + // MARK: Internal + + var mode: RemoteStandard.DisplayMode + @Binding var displayMode: RemoteStandard.DisplayMode + + var body: some View { + Button { + self.displayMode = self.mode + Robot.shared.blacken(.all) + } label: { + ZStack { + Circle() + .fill(.white) + .frame(width: 60, height: 60) + + self.beltSectionIcons + self.earsSectionIcons + } + } + .background(ModeFeedback(backgroundDimension: self.displayMode == self.mode ? 80 : 0)) + } + + // MARK: Private + + private var earsSectionIcons: some View { + HStack { + switch self.mode { + case .fullBelt: + EarIcon() + case .twoHalves, + .fourQuarters: + EarIcon() + EarIcon() + } + } + } + + private var beltSectionIcons: some View { + ZStack { + switch self.mode { + case .fullBelt: + BeltSectionIcon(section: .full(.belt, in: .black)) + case .twoHalves: + BeltSectionIcon(section: .halfLeft(in: .black)) + BeltSectionIcon(section: .halfRight(in: .black)) + case .fourQuarters: + BeltSectionIcon(section: .quarterFrontRight(in: .black)) + BeltSectionIcon(section: .quarterBackRight(in: .black)) + BeltSectionIcon(section: .quarterBackLeft(in: .black)) + BeltSectionIcon(section: .quarterFrontLeft(in: .black)) + } + } + } + } +} + +#Preview { + LedZoneSelectorView.ModeButton(mode: .fullBelt, displayMode: .constant(.fullBelt)) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+ModeFeedback.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+ModeFeedback.swift new file mode 100644 index 0000000000..d20ae93d79 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView+ModeFeedback.swift @@ -0,0 +1,22 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +extension LedZoneSelectorView { + struct ModeFeedback: View { + var backgroundDimension: Int + + var body: some View { + Circle() + .foregroundColor(DesignKitAsset.Colors.btnLightBlue.swiftUIColor) + .frame(width: CGFloat(self.backgroundDimension), height: CGFloat(self.backgroundDimension)) + } + } +} + +#Preview { + LedZoneSelectorView.ModeFeedback(backgroundDimension: 80) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView.swift new file mode 100644 index 0000000000..a0fe7c35a5 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/LedZoneSelector/LedZoneSelectorView.swift @@ -0,0 +1,70 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct LedZoneSelectorView: View { + // MARK: Internal + + let displayMode: RemoteStandard.DisplayMode + + var body: some View { + ZStack { + Circle() + .fill(.white) + .frame(width: 300, height: 300) + + VStack { + Image(systemName: "chevron.up") + .foregroundColor(.gray.opacity(0.7)) + Text("Front") + .foregroundColor(.gray.opacity(0.7)) + + Spacer() + } + .padding(20) + + self.beltSectionButtons + self.earsSectionButtons + } + } + + // MARK: Private + + private var earsSectionButtons: some View { + HStack(spacing: 50) { + switch self.displayMode { + case .fullBelt: + EarButton(selectedEar: .full(.ears, in: .blue)) + case .twoHalves: + EarButton(selectedEar: .earLeft(in: .purple)) + EarButton(selectedEar: .earRight(in: .green)) + case .fourQuarters: + EarButton(selectedEar: .earLeft(in: .orange)) + EarButton(selectedEar: .earRight(in: .blue)) + } + } + } + + private var beltSectionButtons: some View { + ZStack { + switch self.displayMode { + case .fullBelt: + BeltSectionButton(section: .full(.belt, in: .red)) + case .twoHalves: + BeltSectionButton(section: .halfLeft(in: .red)) + BeltSectionButton(section: .halfRight(in: .blue)) + case .fourQuarters: + BeltSectionButton(section: .quarterFrontRight(in: .green)) + BeltSectionButton(section: .quarterBackRight(in: .blue)) + BeltSectionButton(section: .quarterBackLeft(in: .red)) + BeltSectionButton(section: .quarterFrontLeft(in: .yellow)) + } + } + } +} + +#Preview { + LedZoneSelectorView(displayMode: .fourQuarters) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/ReinforcerButton.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/ReinforcerButton.swift new file mode 100644 index 0000000000..bbc9fc2ac0 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/ReinforcerButton.swift @@ -0,0 +1,44 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import RobotKit +import SwiftUI + +// TODO(@ladislas): decide where to put this, keeping it here for now +public extension Robot.Reinforcer { + func icon() -> Image { + switch self { + case .spinBlinkGreenOff: + DesignKitAsset.Reinforcers.spinBlinkGreenOff.swiftUIImage + case .spinBlinkBlueViolet: + DesignKitAsset.Reinforcers.spinBlinkBlueViolet.swiftUIImage + case .fire: + DesignKitAsset.Reinforcers.fire.swiftUIImage + case .sprinkles: + DesignKitAsset.Reinforcers.sprinkles.swiftUIImage + case .rainbow: + DesignKitAsset.Reinforcers.rainbow.swiftUIImage + } + } +} + +// MARK: - ReinforcerButton + +struct ReinforcerButton: View { + var reinforcer: Robot.Reinforcer + let robot = Robot.shared + + var body: some View { + Button { + self.robot.run(self.reinforcer) + } label: { + self.reinforcer.icon() + } + } +} + +#Preview { + ReinforcerButton(reinforcer: .fire) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/RemoteStandard+MainView.swift b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/RemoteStandard+MainView.swift new file mode 100644 index 0000000000..ed849ce01c --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Specialized/RemoteStandard/RemoteStandard+MainView.swift @@ -0,0 +1,79 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import RobotKit +import SwiftUI + +enum RemoteStandard { + enum DisplayMode: String, CaseIterable { + case fullBelt + case twoHalves + case fourQuarters + } + + struct RadialLayout: Layout { + var firstButtonPosX: Int + var firstButtonPosY: Int + var angle: Double + + func sizeThatFits(proposal: ProposedViewSize, subviews _: Subviews, cache _: inout ()) -> CGSize { + proposal.replacingUnspecifiedDimensions() + } + + func placeSubviews(in bounds: CGRect, proposal _: ProposedViewSize, subviews: Subviews, cache _: inout ()) { + let angleDivision = Angle.degrees(self.angle / Double(subviews.count - 1)).radians + let posX = bounds.midX + let posY = bounds.midY * 5 / 4 + + for (index, subview) in subviews.enumerated() { + if index == 0 { + subview.place( + at: CGPoint(x: posX, y: posY), anchor: .center, proposal: .unspecified + ) + } else { + var point = CGPoint(x: firstButtonPosX, y: firstButtonPosY) + .applying(CGAffineTransform(rotationAngle: CGFloat(angleDivision) * CGFloat(index - 1))) + + point.x += posX + point.y += posY + + subview.place(at: point, anchor: .center, proposal: .unspecified) + } + } + } + } + + struct MainView: View { + // MARK: Internal + + var body: some View { + HStack(spacing: 400) { + RadialLayout(firstButtonPosX: -100, firstButtonPosY: -250, angle: 120.0) { + JoystickView() + + ForEach(Robot.Reinforcer.allCases, id: \.self) { reinforcer in + ReinforcerButton(reinforcer: reinforcer) + } + } + + RadialLayout(firstButtonPosX: -120, firstButtonPosY: -200, angle: 90.0) { + LedZoneSelectorView(displayMode: self.displayMode) + + ForEach(DisplayMode.allCases, id: \.self) { mode in + LedZoneSelectorView.ModeButton(mode: mode, displayMode: self.$displayMode) + } + } + } + } + + // MARK: Private + + @State private var displayMode = DisplayMode.fullBelt + } +} + +#Preview { + RemoteStandard.MainView() +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ChoiceColorView.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ChoiceColorView.swift new file mode 100644 index 0000000000..d684ca1789 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ChoiceColorView.swift @@ -0,0 +1,105 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import RobotKit +import SwiftUI + +struct ChoiceColorView: View { + // MARK: Lifecycle + + init(color: String, size: CGFloat, state: GameplayChoiceState = .idle) { + self.color = Robot.Color(from: color) + self.size = size + self.state = state + } + + // MARK: Internal + + // TODO(@ladislas): handle case of color white, add colored border? + var circle: some View { + self.color.screen + .frame( + width: self.size, + height: self.size + ) + .clipShape(Circle()) + } + + var body: some View { + switch self.state { + case .idle: + self.circle + .onAppear { + withAnimation { + self.animationPercent = 0.0 + self.overlayOpacity = 0.0 + } + } + + case .rightAnswer: + self.circle + .overlay { + RightAnswerFeedback(animationPercent: self.animationPercent) + .frame( + width: self.size * self.kOverLayScaleFactor, + height: self.size * self.kOverLayScaleFactor + ) + } + .onAppear { + withAnimation { + self.animationPercent = 1.0 + } + } + + case .wrongAnswer: + self.circle + .overlay { + WrongAnswerFeedback(overlayOpacity: self.overlayOpacity) + .frame( + width: self.size * self.kOverLayScaleFactor, + height: self.size * self.kOverLayScaleFactor + ) + } + .onAppear { + withAnimation { + self.overlayOpacity = 0.8 + } + } + } + } + + // MARK: Private + + private let color: Robot.Color + private let size: CGFloat + private let state: GameplayChoiceState + private let kOverLayScaleFactor: CGFloat = 1.08 + + @State private var animationPercent: CGFloat = .zero + @State private var overlayOpacity: CGFloat = .zero +} + +#Preview { + VStack(spacing: 50) { + HStack(spacing: 50) { + ChoiceColorView(color: "red", size: 200) + ChoiceColorView(color: "red", size: 200, state: .rightAnswer) + ChoiceColorView(color: "red", size: 200, state: .wrongAnswer) + } + + HStack(spacing: 50) { + ChoiceColorView(color: "green", size: 200) + ChoiceColorView(color: "green", size: 200, state: .rightAnswer) + ChoiceColorView(color: "green", size: 200, state: .wrongAnswer) + } + + HStack(spacing: 50) { + ChoiceColorView(color: "blue", size: 200) + ChoiceColorView(color: "blue", size: 200, state: .rightAnswer) + ChoiceColorView(color: "blue", size: 200, state: .wrongAnswer) + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ChoiceEmojiView.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ChoiceEmojiView.swift new file mode 100644 index 0000000000..0a46ce252d --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ChoiceEmojiView.swift @@ -0,0 +1,150 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import RobotKit +import SwiftUI + +// TODO: (@ladislas) Move to UtilsKit +extension String { + func containsEmoji() -> Bool { + contains { $0.isEmoji } + } + + func containsOnlyEmojis() -> Bool { + count > 0 && !contains { !$0.isEmoji } + } +} + +extension Character { + // An emoji can either be a 2 byte unicode character or a normal UTF8 character with an emoji modifier + // appended as is the case with 3️⃣. 0x203C is the first instance of UTF16 emoji that requires no modifier. + // `isEmoji` will evaluate to true for any character that can be turned into an emoji by adding a modifier + // such as the digit "3". To avoid this we confirm that any character below 0x203C has an emoji modifier attached + var isEmoji: Bool { + guard let scalar = unicodeScalars.first else { return false } + return scalar.properties.isEmoji && (scalar.value >= 0x203C || unicodeScalars.count > 1) + } +} + +// MARK: - ChoiceEmojiView + +struct ChoiceEmojiView: View { + // MARK: Lifecycle + + init(emoji: String, size: CGFloat, state: GameplayChoiceState = .idle) { + self.emoji = emoji + self.size = size + self.state = state + } + + // MARK: Internal + + @ViewBuilder + var circle: some View { + Circle() + .fill(self.choiceBackgroundColor) + .overlay { + if self.emoji.count == 1, self.emoji.containsOnlyEmojis() { + Text(self.emoji) + .font(.system(size: self.size / 2)) + } else { + Text("❌\nText is not emoji:\n\(self.emoji)") + .multilineTextAlignment(.center) + .frame( + width: self.size, + height: self.size + ) + .overlay { + Circle() + .stroke(Color.red, lineWidth: 5) + } + } + } + .frame( + width: self.size, + height: self.size + ) + } + + var body: some View { + switch self.state { + case .idle: + self.circle + .onAppear { + withAnimation { + self.animationPercent = 0.0 + self.overlayOpacity = 0.0 + } + } + + case .rightAnswer: + self.circle + .overlay { + RightAnswerFeedback(animationPercent: self.animationPercent) + .frame( + width: self.size * self.kOverLayScaleFactor, + height: self.size * self.kOverLayScaleFactor + ) + } + .onAppear { + withAnimation { + self.animationPercent = 1.0 + } + } + + case .wrongAnswer: + self.circle + .overlay { + WrongAnswerFeedback(overlayOpacity: self.overlayOpacity) + .frame( + width: self.size * self.kOverLayScaleFactor, + height: self.size * self.kOverLayScaleFactor + ) + } + .onAppear { + withAnimation { + self.overlayOpacity = 0.8 + } + } + } + } + + // MARK: Private + + private let choiceBackgroundColor: Color = .init( + light: .white, + dark: UIColor(displayP3Red: 242 / 255, green: 242 / 255, blue: 247 / 255, alpha: 1.0) + ) + + private let emoji: String + private let size: CGFloat + private let state: GameplayChoiceState + private let kOverLayScaleFactor: CGFloat = 1.08 + + @State private var animationPercent: CGFloat = .zero + @State private var overlayOpacity: CGFloat = .zero +} + +#Preview { + VStack(spacing: 40) { + HStack(spacing: 50) { + ChoiceEmojiView(emoji: "🍉", size: 200) + ChoiceEmojiView(emoji: "🍏", size: 200) + } + + HStack(spacing: 40) { + ChoiceEmojiView(emoji: "🌨️", size: 200) + ChoiceEmojiView(emoji: "🌧️", size: 200, state: .rightAnswer) + ChoiceEmojiView(emoji: "☀️", size: 200, state: .wrongAnswer) + } + + HStack(spacing: 40) { + ChoiceEmojiView(emoji: "🐱", size: 200) + ChoiceEmojiView(emoji: "🐶", size: 200) + ChoiceEmojiView(emoji: "🐹🐹", size: 200) + ChoiceEmojiView(emoji: "not_emoji", size: 200) + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ChoiceImageView.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ChoiceImageView.swift new file mode 100644 index 0000000000..8adf70c491 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ChoiceImageView.swift @@ -0,0 +1,122 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import RobotKit +import SwiftUI + +public struct ChoiceImageView: View { + // MARK: Lifecycle + + public init(image: String, size: CGFloat, state: GameplayChoiceState = .idle) { + self.image = image + self.size = size + self.state = state + } + + // MARK: Public + + public var body: some View { + switch self.state { + case .idle: + self.circle + .onAppear { + withAnimation { + self.animationPercent = 0.0 + self.overlayOpacity = 0.0 + } + } + + case .rightAnswer: + self.circle + .overlay { + RightAnswerFeedback(animationPercent: self.animationPercent) + .frame( + width: self.size * self.kOverLayScaleFactor, + height: self.size * self.kOverLayScaleFactor + ) + } + .onAppear { + withAnimation { + self.animationPercent = 1.0 + } + } + + case .wrongAnswer: + self.circle + .overlay { + WrongAnswerFeedback(overlayOpacity: self.overlayOpacity) + .frame( + width: self.size * self.kOverLayScaleFactor, + height: self.size * self.kOverLayScaleFactor + ) + } + .onAppear { + withAnimation { + self.overlayOpacity = 0.8 + } + } + } + } + + // MARK: Internal + + @ViewBuilder + var circle: some View { + if let uiImage = UIImage(named: image) { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fit) + .frame( + width: self.size, + height: self.size + ) + .clipShape(Circle()) + } else { + Text("❌\nImage not found:\n\(self.image)") + .multilineTextAlignment(.center) + .frame( + width: self.size, + height: self.size + ) + .overlay { + Circle() + .stroke(Color.red, lineWidth: 5) + } + } + } + + // MARK: Private + + private let image: String + private let size: CGFloat + private let state: GameplayChoiceState + private let kOverLayScaleFactor: CGFloat = 1.08 + + @State private var animationPercent: CGFloat = .zero + @State private var overlayOpacity: CGFloat = .zero +} + +#Preview { + VStack(spacing: 30) { + HStack(spacing: 50) { + ChoiceImageView(image: "image-placeholder-animals", size: 200) + ChoiceImageView(image: "image-placeholder-missing", size: 200) + } + + HStack(spacing: 50) { + ChoiceImageView(image: "image-placeholder-animals", size: 200) + ChoiceImageView(image: "image-placeholder-animals", size: 200, state: .rightAnswer) + ChoiceImageView(image: "image-placeholder-animals", size: 200, state: .wrongAnswer) + } + + HStack(spacing: 0) { + ChoiceImageView(image: "image-placeholder-animals", size: 200) + ChoiceColorView(color: "blue", size: 200) + + ChoiceImageView(image: "image-placeholder-animals", size: 200, state: .rightAnswer) + ChoiceColorView(color: "blue", size: 200, state: .rightAnswer) + } + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ChoiceSFSymbolView.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ChoiceSFSymbolView.swift new file mode 100644 index 0000000000..548210e29a --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ChoiceSFSymbolView.swift @@ -0,0 +1,130 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import RobotKit +import SwiftUI + +struct ChoiceSFSymbolView: View { + // MARK: Lifecycle + + init(image: String, size: CGFloat, state: GameplayChoiceState = .idle) { + self.sfsymbol = image + self.size = size + self.state = state + } + + // MARK: Internal + + @ViewBuilder + var circle: some View { + Circle() + .fill(self.choiceBackgroundColor) + .overlay { + if UIImage(systemName: self.sfsymbol) != nil { + Image(systemName: self.sfsymbol) + .resizable() + .scaledToFit() + .scaleEffect(0.5) + } else { + Text("❌\nSF Symbol not found:\n\(self.sfsymbol)") + .multilineTextAlignment(.center) + .frame( + width: self.size, + height: self.size + ) + .overlay { + Circle() + .stroke(Color.red, lineWidth: 5) + } + } + } + .foregroundStyle(.black) + .frame( + width: self.size, + height: self.size + ) + } + + var body: some View { + switch self.state { + case .idle: + self.circle + .onAppear { + withAnimation { + self.animationPercent = 0.0 + self.overlayOpacity = 0.0 + } + } + + case .rightAnswer: + self.circle + .overlay { + RightAnswerFeedback(animationPercent: self.animationPercent) + .frame( + width: self.size * self.kOverLayScaleFactor, + height: self.size * self.kOverLayScaleFactor + ) + } + .onAppear { + withAnimation { + self.animationPercent = 1.0 + } + } + + case .wrongAnswer: + self.circle + .overlay { + WrongAnswerFeedback(overlayOpacity: self.overlayOpacity) + .frame( + width: self.size * self.kOverLayScaleFactor, + height: self.size * self.kOverLayScaleFactor + ) + } + .onAppear { + withAnimation { + self.overlayOpacity = 0.8 + } + } + } + } + + // MARK: Private + + private let choiceBackgroundColor: Color = .init( + light: .white, + dark: UIColor(displayP3Red: 242 / 255, green: 242 / 255, blue: 247 / 255, alpha: 1.0) + ) + + private let sfsymbol: String + private let size: CGFloat + private let state: GameplayChoiceState + private let kOverLayScaleFactor: CGFloat = 1.08 + + @State private var animationPercent: CGFloat = .zero + @State private var overlayOpacity: CGFloat = .zero +} + +#Preview { + VStack(spacing: 30) { + HStack(spacing: 40) { + ChoiceSFSymbolView(image: "airplane", size: 200) + ChoiceSFSymbolView(image: "paperplane", size: 200) + } + + HStack(spacing: 40) { + ChoiceSFSymbolView(image: "sunrise", size: 200) + ChoiceSFSymbolView(image: "sparkles", size: 200, state: .rightAnswer) + ChoiceSFSymbolView(image: "cloud.drizzle", size: 200, state: .wrongAnswer) + } + + HStack(spacing: 40) { + ChoiceSFSymbolView(image: "cat", size: 200) + ChoiceSFSymbolView(image: "fish", size: 200) + ChoiceSFSymbolView(image: "carrot", size: 200) + ChoiceSFSymbolView(image: "not_a_real_sumbol", size: 200) + } + } + .background(.lkBackground) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+1_OneChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+1_OneChoice.swift new file mode 100644 index 0000000000..ee0985eb32 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+1_OneChoice.swift @@ -0,0 +1,37 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension ListenThenTouchToSelectView { + struct OneChoiceView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + let choice = self.viewModel.choices[0] + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + + // MARK: Private + + private let kAnswerSize: CGFloat = 300 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + ] + + return ListenThenTouchToSelectView( + choices: choices, audioRecording: AudioRecording(name: "drums", file: "drums") + ) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+2_TwoChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+2_TwoChoice.swift new file mode 100644 index 0000000000..2e4f986493 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+2_TwoChoice.swift @@ -0,0 +1,42 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension ListenThenTouchToSelectView { + struct TwoChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 100 + private let kAnswerSize: CGFloat = 300 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + ] + + return ListenThenTouchToSelectView( + choices: choices, audioRecording: AudioRecording(name: "drums", file: "drums") + ) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+3_ThreeChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+3_ThreeChoice.swift new file mode 100644 index 0000000000..d7708d903f --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+3_ThreeChoice.swift @@ -0,0 +1,51 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension ListenThenTouchToSelectView { + struct ThreeChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + VStack(spacing: self.kVerticalSpacing) { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[0...1]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + + TouchToSelectChoiceView(choice: self.viewModel.choices[2], size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[2]) + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 200 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 240 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "green", type: .color, isRightAnswer: false), + ] + + return ListenThenTouchToSelectView( + choices: choices, audioRecording: AudioRecording(name: "drums", file: "drums") + ) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+4_FourChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+4_FourChoice.swift new file mode 100644 index 0000000000..f5e489f720 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+4_FourChoice.swift @@ -0,0 +1,56 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension ListenThenTouchToSelectView { + struct FourChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + VStack(spacing: self.kVerticalSpacing) { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[0...1]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[2...3]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 200 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 240 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "green", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "yellow", type: .color, isRightAnswer: false), + ] + + return ListenThenTouchToSelectView( + choices: choices, audioRecording: AudioRecording(name: "drums", file: "drums") + ) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+5_FiveChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+5_FiveChoice.swift new file mode 100644 index 0000000000..55e112f295 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+5_FiveChoice.swift @@ -0,0 +1,57 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension ListenThenTouchToSelectView { + struct FiveChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + VStack(spacing: self.kVerticalSpacing) { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[0...2]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[3...4]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 200 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "green", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "yellow", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "purple", type: .color, isRightAnswer: false), + ] + + return ListenThenTouchToSelectView( + choices: choices, audioRecording: AudioRecording(name: "drums", file: "drums") + ) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+6_SixChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+6_SixChoice.swift new file mode 100644 index 0000000000..8d5ba5eb87 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView+6_SixChoice.swift @@ -0,0 +1,58 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension ListenThenTouchToSelectView { + struct SixChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + VStack(spacing: self.kVerticalSpacing) { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[0...2]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[3...5]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 200 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "green", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "yellow", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "purple", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "lightBlue", type: .color, isRightAnswer: false), + ] + + return ListenThenTouchToSelectView( + choices: choices, audioRecording: AudioRecording(name: "drums", file: "drums") + ) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView.swift new file mode 100644 index 0000000000..171a63baf2 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ListenThenTouchToSelect/ListenThenTouchToSelectView.swift @@ -0,0 +1,114 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import SwiftUI + +public struct ListenThenTouchToSelectView: View { + // MARK: Lifecycle + + public init(choices: [TouchToSelect.Choice], audioRecording: AudioRecording) { + _viewModel = StateObject(wrappedValue: TouchToSelectViewViewModel(choices: choices)) + _audioPlayer = StateObject(wrappedValue: AudioPlayer(audioRecording: audioRecording)) + } + + public init(exercise: Exercise, data: ExerciseSharedData? = nil) { + guard let payload = exercise.payload as? TouchToSelect.Payload, + case let .ipad(type: .audio(name)) = exercise.action + else { + log.error("Exercise payload is not .selection and/or Exercise does not contain iPad audio action") + fatalError("💥 Exercise payload is not .selection and/or Exercise does not contain iPad audio action") + } + + _viewModel = StateObject( + wrappedValue: TouchToSelectViewViewModel(choices: payload.choices, shared: data)) + + let audioRecording = AudioRecording(name: name, file: name) + _audioPlayer = StateObject(wrappedValue: AudioPlayer(audioRecording: audioRecording)) + } + + // MARK: Public + + public var body: some View { + let interface = Interface(rawValue: viewModel.choices.count) + + HStack(spacing: 0) { + ActionButtonListen(audioPlayer: self.audioPlayer) + .padding(20) + + Divider() + .opacity(0.4) + .frame(maxHeight: 500) + .padding(.vertical, 20) + + Spacer() + + switch interface { + case .oneChoice: + OneChoiceView(viewModel: self.viewModel, isTappable: self.audioPlayer.didFinishPlaying) + .onTapGestureIf(self.audioPlayer.didFinishPlaying) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.audioPlayer.didFinishPlaying) + + case .twoChoices: + TwoChoicesView(viewModel: self.viewModel, isTappable: self.audioPlayer.didFinishPlaying) + .onTapGestureIf(self.audioPlayer.didFinishPlaying) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.audioPlayer.didFinishPlaying) + + case .threeChoices: + ThreeChoicesView(viewModel: self.viewModel, isTappable: self.audioPlayer.didFinishPlaying) + .onTapGestureIf(self.audioPlayer.didFinishPlaying) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.audioPlayer.didFinishPlaying) + + case .fourChoices: + FourChoicesView(viewModel: self.viewModel, isTappable: self.audioPlayer.didFinishPlaying) + .onTapGestureIf(self.audioPlayer.didFinishPlaying) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.audioPlayer.didFinishPlaying) + + case .fiveChoices: + FiveChoicesView(viewModel: self.viewModel, isTappable: self.audioPlayer.didFinishPlaying) + .onTapGestureIf(self.audioPlayer.didFinishPlaying) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.audioPlayer.didFinishPlaying) + + case .sixChoices: + SixChoicesView(viewModel: self.viewModel, isTappable: self.audioPlayer.didFinishPlaying) + .onTapGestureIf(self.audioPlayer.didFinishPlaying) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.audioPlayer.didFinishPlaying) + + default: + ProgressView() + } + + Spacer() + } + } + + // MARK: Internal + + enum Interface: Int { + case oneChoice = 1 + case twoChoices + case threeChoices + case fourChoices + case fiveChoices + case sixChoices + } + + // MARK: Private + + @StateObject private var viewModel: TouchToSelectViewViewModel + @StateObject private var audioPlayer: AudioPlayer +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelect.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelect.swift new file mode 100644 index 0000000000..06c22ef631 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelect.swift @@ -0,0 +1,111 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import SwiftUI + +public struct ObserveThenTouchToSelectView: View { + // MARK: Lifecycle + + public init(choices: [TouchToSelect.Choice], image: String) { + _viewModel = StateObject(wrappedValue: TouchToSelectViewViewModel(choices: choices)) + self.image = image + } + + public init(exercise: Exercise, data: ExerciseSharedData? = nil) { + guard let payload = exercise.payload as? TouchToSelect.Payload, + case let .ipad(type: .image(name)) = exercise.action + else { + log.error("Exercise payload is not .selection and/or Exercise does not contain iPad image action") + fatalError("💥 Exercise payload is not .selection and/or Exercise does not contain iPad image action") + } + + _viewModel = StateObject( + wrappedValue: TouchToSelectViewViewModel(choices: payload.choices, shared: data)) + + self.image = name + } + + // MARK: Public + + public var body: some View { + let interface = Interface(rawValue: viewModel.choices.count) + + HStack(spacing: 0) { + ActionButtonObserve(image: self.image, imageWasTapped: self.$imageWasTapped) + .padding(20) + + Spacer() + + switch interface { + case .oneChoice: + OneChoiceView(viewModel: self.viewModel, isTappable: self.imageWasTapped) + .onTapGestureIf(self.imageWasTapped) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.imageWasTapped) + + case .twoChoices: + TwoChoicesView(viewModel: self.viewModel, isTappable: self.imageWasTapped) + .onTapGestureIf(self.imageWasTapped) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.imageWasTapped) + + case .threeChoices: + ThreeChoicesView(viewModel: self.viewModel, isTappable: self.imageWasTapped) + .onTapGestureIf(self.imageWasTapped) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.imageWasTapped) + + case .fourChoices: + FourChoicesView(viewModel: self.viewModel, isTappable: self.imageWasTapped) + .onTapGestureIf(self.imageWasTapped) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.imageWasTapped) + + case .fiveChoices: + FiveChoicesView(viewModel: self.viewModel, isTappable: self.imageWasTapped) + .onTapGestureIf(self.imageWasTapped) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.imageWasTapped) + + case .sixChoices: + SixChoicesView(viewModel: self.viewModel, isTappable: self.imageWasTapped) + .onTapGestureIf(self.imageWasTapped) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.imageWasTapped) + + default: + ProgressView() + } + + Spacer() + } + } + + // MARK: Internal + + enum Interface: Int { + case oneChoice = 1 + case twoChoices + case threeChoices + case fourChoices + case fiveChoices + case sixChoices + } + + // MARK: Private + + @StateObject private var viewModel: TouchToSelectViewViewModel + + @State private var imageWasTapped = false + + private let image: String +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+1_OneChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+1_OneChoice.swift new file mode 100644 index 0000000000..a01e6f1b13 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+1_OneChoice.swift @@ -0,0 +1,35 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension ObserveThenTouchToSelectView { + struct OneChoiceView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + let choice = self.viewModel.choices[0] + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + + // MARK: Private + + private let kAnswerSize: CGFloat = 180 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + ] + + return ObserveThenTouchToSelectView(choices: choices, image: "image-landscape-blue") +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+2_TwoChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+2_TwoChoice.swift new file mode 100644 index 0000000000..ff12578ce4 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+2_TwoChoice.swift @@ -0,0 +1,40 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension ObserveThenTouchToSelectView { + struct TwoChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kAnswerSize: CGFloat = 180 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + ] + + return ObserveThenTouchToSelectView(choices: choices, image: "image-landscape-blue") +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+3_ThreeChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+3_ThreeChoice.swift new file mode 100644 index 0000000000..c336840a8d --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+3_ThreeChoice.swift @@ -0,0 +1,49 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension ObserveThenTouchToSelectView { + struct ThreeChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + VStack(spacing: self.kVerticalSpacing) { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[0...1]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + + TouchToSelectChoiceView(choice: self.viewModel.choices[2], size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[2]) + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 180 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "green", type: .color, isRightAnswer: false), + ] + + return ObserveThenTouchToSelectView(choices: choices, image: "image-landscape-blue") +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+4_FourChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+4_FourChoice.swift new file mode 100644 index 0000000000..8cd4a07840 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+4_FourChoice.swift @@ -0,0 +1,54 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension ObserveThenTouchToSelectView { + struct FourChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + VStack(spacing: self.kVerticalSpacing) { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[0...1]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[2...3]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 180 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "green", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "yellow", type: .color, isRightAnswer: false), + ] + + return ObserveThenTouchToSelectView(choices: choices, image: "image-landscape-blue") +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+5_FiveChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+5_FiveChoice.swift new file mode 100644 index 0000000000..0e313f1045 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+5_FiveChoice.swift @@ -0,0 +1,55 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension ObserveThenTouchToSelectView { + struct FiveChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + VStack(spacing: self.kVerticalSpacing) { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[0...2]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[3...4]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 40 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 140 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "green", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "yellow", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "purple", type: .color, isRightAnswer: false), + ] + + return ObserveThenTouchToSelectView(choices: choices, image: "image-landscape-blue") +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+6_SixChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+6_SixChoice.swift new file mode 100644 index 0000000000..c333985521 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/ObserveThenTouchToSelect/ObserveThenTouchToSelectView+6_SixChoice.swift @@ -0,0 +1,56 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension ObserveThenTouchToSelectView { + struct SixChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + VStack(spacing: self.kVerticalSpacing) { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[0...2]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[3...5]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 40 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 140 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "green", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "yellow", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "purple", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "lightBlue", type: .color, isRightAnswer: false), + ] + + return ObserveThenTouchToSelectView(choices: choices, image: "image-landscape-blue") +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+1_OneChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+1_OneChoice.swift new file mode 100644 index 0000000000..d652af44b1 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+1_OneChoice.swift @@ -0,0 +1,35 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension RobotThenTouchToSelectView { + struct OneChoiceView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + let choice = self.viewModel.choices[0] + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + + // MARK: Private + + private let kAnswerSize: CGFloat = 300 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + ] + + return RobotThenTouchToSelectView(choices: choices) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+2_TwoChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+2_TwoChoice.swift new file mode 100644 index 0000000000..8e8d0b3d53 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+2_TwoChoice.swift @@ -0,0 +1,40 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension RobotThenTouchToSelectView { + struct TwoChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 100 + private let kAnswerSize: CGFloat = 300 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + ] + + return RobotThenTouchToSelectView(choices: choices) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+3_ThreeChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+3_ThreeChoice.swift new file mode 100644 index 0000000000..6b91ec9af5 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+3_ThreeChoice.swift @@ -0,0 +1,49 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension RobotThenTouchToSelectView { + struct ThreeChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + VStack(spacing: self.kVerticalSpacing) { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[0...1]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + + TouchToSelectChoiceView(choice: self.viewModel.choices[2], size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[2]) + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 200 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 240 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "green", type: .color, isRightAnswer: false), + ] + + return RobotThenTouchToSelectView(choices: choices) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+4_FourChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+4_FourChoice.swift new file mode 100644 index 0000000000..a0e63274bc --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+4_FourChoice.swift @@ -0,0 +1,54 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension RobotThenTouchToSelectView { + struct FourChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + VStack(spacing: self.kVerticalSpacing) { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[0...1]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[2...3]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 200 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 240 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "green", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "yellow", type: .color, isRightAnswer: false), + ] + + return RobotThenTouchToSelectView(choices: choices) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+5_FiveChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+5_FiveChoice.swift new file mode 100644 index 0000000000..c3a7b542c6 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+5_FiveChoice.swift @@ -0,0 +1,55 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension RobotThenTouchToSelectView { + struct FiveChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + VStack(spacing: self.kVerticalSpacing) { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[0...2]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[3...4]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 200 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "green", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "yellow", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "purple", type: .color, isRightAnswer: false), + ] + + return RobotThenTouchToSelectView(choices: choices) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+6_SixChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+6_SixChoice.swift new file mode 100644 index 0000000000..4ac03cc26e --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView+6_SixChoice.swift @@ -0,0 +1,56 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension RobotThenTouchToSelectView { + struct SixChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + let isTappable: Bool + + var body: some View { + VStack(spacing: self.kVerticalSpacing) { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[0...2]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[3...5]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize, isTappable: self.isTappable) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 200 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "green", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "yellow", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "purple", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "lightBlue", type: .color, isRightAnswer: false), + ] + + return RobotThenTouchToSelectView(choices: choices) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView.swift new file mode 100644 index 0000000000..ad14a05d95 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/RobotThenTouchToSelect/RobotThenTouchToSelectView.swift @@ -0,0 +1,150 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import DesignKit +import RobotKit +import SwiftUI + +public struct RobotThenTouchToSelectView: View { + // MARK: Lifecycle + + public init(choices: [TouchToSelect.Choice]) { + _viewModel = StateObject(wrappedValue: TouchToSelectViewViewModel(choices: choices)) + + self.actionType = .color("red") + } + + public init(exercise: Exercise, data: ExerciseSharedData? = nil) { + guard let payload = exercise.payload as? TouchToSelect.Payload, + case let .robot(actionType) = exercise.action + else { + log.error("Exercise payload is not .selection and/or Exercise does not contain robot action") + fatalError("💥 Exercise payload is not .selection and/or Exercise does not contain robot action") + } + + _viewModel = StateObject( + wrappedValue: TouchToSelectViewViewModel(choices: payload.choices, shared: data)) + + self.actionType = actionType + + self.robot.blacken(.all) + } + + // MARK: Public + + public var body: some View { + let interface = Interface(rawValue: viewModel.choices.count) + + HStack(spacing: 0) { + Button { + switch self.actionType { + case let .color(value): + self.robot.shine(.all(in: .init(from: value))) + case .audio, + .image, + .speech: + log.error("Action not available for robot: \(self.actionType)") + fatalError("💥 Action not available for robot: \(self.actionType)") + } + + withAnimation { + self.didSendCommandToRobot = true + } + } label: { + Image(uiImage: DesignKitAsset.Images.robotFaceSimple.image) + .resizable() + .frame(width: 200, height: 200) + .padding() + } + .disabled(self.didSendCommandToRobot) + .scaleEffect(self.didSendCommandToRobot ? 1.0 : 0.8, anchor: .center) + .shadow( + color: .accentColor.opacity(0.2), + radius: self.didSendCommandToRobot ? 6 : 3, + x: 0, + y: 3 + ) + .animation(.spring(response: 1, dampingFraction: 0.45), value: self.didSendCommandToRobot) + .padding(20) + + Divider() + .opacity(0.4) + .frame(maxHeight: 500) + .padding(.vertical, 20) + + Spacer() + + switch interface { + case .oneChoice: + OneChoiceView(viewModel: self.viewModel, isTappable: self.didSendCommandToRobot) + .onTapGestureIf(self.didSendCommandToRobot) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.didSendCommandToRobot) + + case .twoChoices: + TwoChoicesView(viewModel: self.viewModel, isTappable: self.didSendCommandToRobot) + .onTapGestureIf(self.didSendCommandToRobot) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.didSendCommandToRobot) + + case .threeChoices: + ThreeChoicesView(viewModel: self.viewModel, isTappable: self.didSendCommandToRobot) + .onTapGestureIf(self.didSendCommandToRobot) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.didSendCommandToRobot) + + case .fourChoices: + FourChoicesView(viewModel: self.viewModel, isTappable: self.didSendCommandToRobot) + .onTapGestureIf(self.didSendCommandToRobot) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.didSendCommandToRobot) + + case .fiveChoices: + FiveChoicesView(viewModel: self.viewModel, isTappable: self.didSendCommandToRobot) + .onTapGestureIf(self.didSendCommandToRobot) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.didSendCommandToRobot) + + case .sixChoices: + SixChoicesView(viewModel: self.viewModel, isTappable: self.didSendCommandToRobot) + .onTapGestureIf(self.didSendCommandToRobot) { + self.viewModel.onChoiceTapped(choice: self.viewModel.choices[0]) + } + .animation(.easeOut(duration: 0.3), value: self.didSendCommandToRobot) + + default: + ProgressView() + } + + Spacer() + } + } + + // MARK: Internal + + enum Interface: Int { + case oneChoice = 1 + case twoChoices + case threeChoices + case fourChoices + case fiveChoices + case sixChoices + } + + let robot = Robot.shared + + // MARK: Private + + @StateObject private var viewModel: TouchToSelectViewViewModel + @State private var didSendCommandToRobot = false + + private let actionType: Exercise.Action.ActionType +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectChoiceView.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectChoiceView.swift new file mode 100644 index 0000000000..cae79b01ba --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectChoiceView.swift @@ -0,0 +1,75 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +struct TouchToSelectChoiceView: View { + // MARK: Lifecycle + + private init(choice: TouchToSelect.Choice, state: GameplayChoiceState, size: CGFloat, isTappable: Bool = true) { + self.choice = choice + self.state = state + self.size = size + self.isTappable = isTappable + } + + init(choice: GameplayTouchToSelectChoiceModel, size: CGFloat, isTappable: Bool = true) { + self.init(choice: choice.choice, state: choice.state, size: size, isTappable: isTappable) + } + + // MARK: Internal + + let choice: TouchToSelect.Choice + let state: GameplayChoiceState + + let size: CGFloat + var isTappable = true + + var body: some View { + Group { + switch self.choice.type { + case .color: + ChoiceColorView(color: self.choice.value, size: self.size, state: self.state) + .overlay( + Circle() + .fill(self.isTappable ? .clear : .white.opacity(0.6)) + ) + .animation(.easeOut(duration: 0.3), value: self.isTappable) + + case .image: + ChoiceImageView(image: self.choice.value, size: self.size, state: self.state) + .overlay( + Circle() + .fill(self.isTappable ? .clear : .white.opacity(0.6)) + ) + .animation(.easeOut(duration: 0.3), value: self.isTappable) + + case .sfsymbol: + ChoiceSFSymbolView(image: self.choice.value, size: self.size, state: self.state) + .overlay( + Circle() + .fill(self.isTappable ? .clear : .white.opacity(0.6)) + ) + .animation(.easeOut(duration: 0.3), value: self.isTappable) + + case .emoji: + ChoiceEmojiView(emoji: self.choice.value, size: self.size, state: self.state) + .overlay( + Circle() + .fill(self.isTappable ? .clear : .white.opacity(0.6)) + ) + .animation(.easeOut(duration: 0.3), value: self.isTappable) + + default: + Text("❌ ERROR\nChoice type not implemented") + .multilineTextAlignment(.center) + .onAppear { + log.error("Choice type \(self.choice.type) not implemented for choice: \(self.choice)") + } + } + } + .contentShape(Circle()) + } +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+1_OneChoice.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+1_OneChoice.swift new file mode 100644 index 0000000000..13d115e651 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+1_OneChoice.swift @@ -0,0 +1,34 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension TouchToSelectView { + struct OneChoiceView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + + var body: some View { + let choice = self.viewModel.choices[0] + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + + // MARK: Private + + private let kAnswerSize: CGFloat = 300 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + ] + + return TouchToSelectView(choices: choices) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+2_TwoChoices.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+2_TwoChoices.swift new file mode 100644 index 0000000000..a10502a09b --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+2_TwoChoices.swift @@ -0,0 +1,39 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension TouchToSelectView { + struct TwoChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + + var body: some View { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 150 + private let kAnswerSize: CGFloat = 300 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "image-placeholder-food", type: .image, isRightAnswer: false), + ] + + return TouchToSelectView(choices: choices) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+3_ThreeChoices.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+3_ThreeChoices.swift new file mode 100644 index 0000000000..0491e38eca --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+3_ThreeChoices.swift @@ -0,0 +1,40 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension TouchToSelectView { + struct ThreeChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + + var body: some View { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[0...2]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 80 + private let kAnswerSize: CGFloat = 280 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "image-placeholder-food", type: .image, isRightAnswer: false), + TouchToSelect.Choice(value: "image-placeholder-portrait", type: .image, isRightAnswer: false), + ] + + return TouchToSelectView(choices: choices) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+4_FourChoices.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+4_FourChoices.swift new file mode 100644 index 0000000000..1c01e31fa3 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+4_FourChoices.swift @@ -0,0 +1,53 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension TouchToSelectView { + struct FourChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + + var body: some View { + VStack(spacing: self.kVerticalSpacing) { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[0...1]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[2...3]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 200 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 240 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "image-placeholder-food", type: .image, isRightAnswer: false), + TouchToSelect.Choice(value: "image-placeholder-portrait", type: .image, isRightAnswer: false), + ] + + return TouchToSelectView(choices: choices) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+5_FiveChoices.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+5_FiveChoices.swift new file mode 100644 index 0000000000..6008fd33ec --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+5_FiveChoices.swift @@ -0,0 +1,54 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension TouchToSelectView { + struct FiveChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + + var body: some View { + VStack(spacing: self.kVerticalSpacing) { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[0...2]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[3...4]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 240 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "green", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "image-placeholder-food", type: .image, isRightAnswer: false), + TouchToSelect.Choice(value: "image-placeholder-portrait", type: .image, isRightAnswer: false), + ] + + return TouchToSelectView(choices: choices) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+6_SixChoices.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+6_SixChoices.swift new file mode 100644 index 0000000000..306692578f --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView+6_SixChoices.swift @@ -0,0 +1,55 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SwiftUI + +extension TouchToSelectView { + struct SixChoicesView: View { + // MARK: Internal + + @ObservedObject var viewModel: TouchToSelectViewViewModel + + var body: some View { + VStack(spacing: self.kVerticalSpacing) { + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[0...2]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + + HStack(spacing: self.kHorizontalSpacing) { + ForEach(self.viewModel.choices[3...5]) { choice in + TouchToSelectChoiceView(choice: choice, size: self.kAnswerSize) + .onTapGesture { + self.viewModel.onChoiceTapped(choice: choice) + } + } + } + } + } + + // MARK: Private + + private let kHorizontalSpacing: CGFloat = 60 + private let kVerticalSpacing: CGFloat = 40 + private let kAnswerSize: CGFloat = 240 + } +} + +#Preview { + let choices: [TouchToSelect.Choice] = [ + TouchToSelect.Choice(value: "red", type: .color, isRightAnswer: true), + TouchToSelect.Choice(value: "blue", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "green", type: .color, isRightAnswer: false), + TouchToSelect.Choice(value: "image-placeholder-animals", type: .image, isRightAnswer: false), + TouchToSelect.Choice(value: "image-placeholder-food", type: .image, isRightAnswer: false), + TouchToSelect.Choice(value: "image-placeholder-portrait", type: .image, isRightAnswer: false), + ] + + return TouchToSelectView(choices: choices) +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView.swift new file mode 100644 index 0000000000..3c1e1e50f7 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectView.swift @@ -0,0 +1,70 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import SwiftUI + +public struct TouchToSelectView: View { + // MARK: Lifecycle + + public init(choices: [TouchToSelect.Choice], shuffle: Bool = false) { + _viewModel = StateObject(wrappedValue: TouchToSelectViewViewModel(choices: choices, shuffle: shuffle)) + } + + public init(exercise: Exercise, data: ExerciseSharedData? = nil) { + guard let payload = exercise.payload as? TouchToSelect.Payload else { + fatalError("Exercise payload is not .selection") + } + + _viewModel = StateObject( + wrappedValue: TouchToSelectViewViewModel( + choices: payload.choices, shuffle: payload.shuffleChoices, shared: data + )) + } + + // MARK: Public + + public var body: some View { + let interface = Interface(rawValue: viewModel.choices.count) + + switch interface { + case .oneChoice: + OneChoiceView(viewModel: self.viewModel) + + case .twoChoices: + TwoChoicesView(viewModel: self.viewModel) + + case .threeChoices: + ThreeChoicesView(viewModel: self.viewModel) + + case .fourChoices: + FourChoicesView(viewModel: self.viewModel) + + case .fiveChoices: + FiveChoicesView(viewModel: self.viewModel) + + case .sixChoices: + SixChoicesView(viewModel: self.viewModel) + + default: + ProgressView() + } + } + + // MARK: Internal + + enum Interface: Int { + case oneChoice = 1 + case twoChoices + case threeChoices + case fourChoices + case fiveChoices + case sixChoices + } + + // MARK: Private + + @StateObject private var viewModel: TouchToSelectViewViewModel +} diff --git a/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectViewViewModel.swift b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectViewViewModel.swift new file mode 100644 index 0000000000..edae5b6cf6 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Exercises/Touch/TouchToSelect/TouchToSelectViewViewModel.swift @@ -0,0 +1,55 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import SwiftUI + +class TouchToSelectViewViewModel: ObservableObject { + // MARK: Lifecycle + + init(choices: [TouchToSelect.Choice], shuffle: Bool = false, shared: ExerciseSharedData? = nil) { + self.gameplay = GameplayFindTheRightAnswers( + choices: choices.map { GameplayTouchToSelectChoiceModel(choice: $0) }, shuffle: shuffle + ) + self.exercicesSharedData = shared ?? ExerciseSharedData() + + self.subscribeToGameplaySelectionChoicesUpdates() + self.subscribeToGameplayStateUpdates() + } + + // MARK: Public + + public func onChoiceTapped(choice: GameplayTouchToSelectChoiceModel) { + self.gameplay.process(choice) + } + + // MARK: Internal + + @Published var choices: [GameplayTouchToSelectChoiceModel] = [] + @ObservedObject var exercicesSharedData: ExerciseSharedData + + // MARK: Private + + private let gameplay: GameplayFindTheRightAnswers + private var cancellables: Set = [] + + private func subscribeToGameplaySelectionChoicesUpdates() { + self.gameplay.choices + .receive(on: DispatchQueue.main) + .sink { + self.choices = $0 + } + .store(in: &self.cancellables) + } + + private func subscribeToGameplayStateUpdates() { + self.gameplay.state + .receive(on: DispatchQueue.main) + .sink { + self.exercicesSharedData.state = $0 + } + .store(in: &self.cancellables) + } +} diff --git a/Modules/GameEngineKit/Sources/Extensions/Text+MarkdownInit.swift b/Modules/GameEngineKit/Sources/Extensions/Text+MarkdownInit.swift new file mode 100644 index 0000000000..acac998222 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Extensions/Text+MarkdownInit.swift @@ -0,0 +1,11 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +public extension Text { + init(markdown: String) { + self.init(.init(markdown)) + } +} diff --git a/Modules/GameEngineKit/Sources/Extensions/UIApplication+keyWindow.swift b/Modules/GameEngineKit/Sources/Extensions/UIApplication+keyWindow.swift new file mode 100644 index 0000000000..427e90b610 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Extensions/UIApplication+keyWindow.swift @@ -0,0 +1,24 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +extension UIApplication { + var keyWindow: UIWindow? { + // Get connected scenes + self.connectedScenes + // Keep only active scenes, onscreen and visible to the user + .filter { $0.activationState == .foregroundActive } + // Keep only the first `UIWindowScene` + .first(where: { $0 is UIWindowScene }) + // Get its associated windows + .flatMap { $0 as? UIWindowScene }?.windows + // Finally, keep only the key window + .first(where: \.isKeyWindow) + } + + func dismissAll(animated: Bool, completion: (() -> Void)? = nil) { + self.keyWindow?.rootViewController?.dismiss(animated: animated, completion: completion) + } +} diff --git a/Modules/GameEngineKit/Sources/GameEngineKit.swift b/Modules/GameEngineKit/Sources/GameEngineKit.swift new file mode 100644 index 0000000000..e300b2838e --- /dev/null +++ b/Modules/GameEngineKit/Sources/GameEngineKit.swift @@ -0,0 +1,7 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LogKit + +let log = LogKit.createLoggerFor(module: "GameEngineKit") diff --git a/Modules/GameEngineKit/Sources/Gameplays/AssociateCategories/GameplayAssociateCategories+DragAndDrop.swift b/Modules/GameEngineKit/Sources/Gameplays/AssociateCategories/GameplayAssociateCategories+DragAndDrop.swift new file mode 100644 index 0000000000..d94d8cba0a --- /dev/null +++ b/Modules/GameEngineKit/Sources/Gameplays/AssociateCategories/GameplayAssociateCategories+DragAndDrop.swift @@ -0,0 +1,46 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import Foundation + +// MARK: - GameplayAssociateCategoriesChoiceModel + +struct GameplayAssociateCategoriesChoiceModel: GameplayChoiceModelProtocol { + typealias ChoiceType = DragAndDropToAssociate.Choice + + let id: String = UUID().uuidString + let choice: ChoiceType + var state: GameplayChoiceState = .idle +} + +extension GameplayAssociateCategories where ChoiceModelType == GameplayAssociateCategoriesChoiceModel { + convenience init(choices: [GameplayAssociateCategoriesChoiceModel], shuffle _: Bool = false, allowedTrials: Int? = nil) { + self.init() + self.choices.send(choices) + state.send(.playing) + + if let allowedTrials { + self.allowedTrials = allowedTrials + } else { + self.allowedTrials = getNumberOfAllowedTrials(from: kGradingLUTDefault) + } + } + + func process(_ choice: ChoiceModelType, _ destination: ChoiceModelType) { + numberOfTrials += 1 + + if choice.choice.category == destination.choice.category { + updateChoice(choice, state: .rightAnswer) + updateChoice(destination, state: .rightAnswer) + } else { + updateChoice(choice, state: .wrongAnswer) + } + + if choices.value.allSatisfy({ $0.state == .rightAnswer }) { + let level = evaluateCompletionLevel(allowedTrials: allowedTrials, numberOfTrials: numberOfTrials) + state.send(.completed(level: level)) + } + } +} diff --git a/Modules/GameEngineKit/Sources/Gameplays/AssociateCategories/GameplayAssociateCategories.swift b/Modules/GameEngineKit/Sources/Gameplays/AssociateCategories/GameplayAssociateCategories.swift new file mode 100644 index 0000000000..e4e5689302 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Gameplays/AssociateCategories/GameplayAssociateCategories.swift @@ -0,0 +1,37 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import Foundation + +class GameplayAssociateCategories: StatefulGameplayProtocol + where ChoiceModelType: GameplayChoiceModelProtocol +{ + var choices: CurrentValueSubject<[GameplayAssociateCategoriesChoiceModel], Never> = .init([]) + var state: CurrentValueSubject = .init(.idle) + var numberOfTrials: Int = 0 + var allowedTrials: Int = 0 + + func updateChoice(_ choice: ChoiceModelType, state: GameplayChoiceState) { + guard let index = choices.value.firstIndex(where: { $0.id == choice.id }) else { + return + } + self.choices.value[index].state = state + } + + func getNumberOfRightAnswers(choices: [GameplayAssociateCategoriesChoiceModel]) -> Int { + let numberOfCategories = Set(choices.map(\.choice.category)).count + let numberOfCategorizableChoices = choices.map { $0.choice.category != .none }.count + + return numberOfCategorizableChoices - numberOfCategories + } + + func getNumberOfAllowedTrials(from table: GradingLUT) -> Int { + let numberOfChoices = self.choices.value.count + let numberOfRightAnswers = self.getNumberOfRightAnswers(choices: self.choices.value) + + return table[numberOfChoices]![numberOfRightAnswers]! + } +} diff --git a/Modules/GameEngineKit/Sources/Gameplays/FindTheRightAnswers/GameplayFindTheRightAnswers+DragAndDropIntoZones.swift b/Modules/GameEngineKit/Sources/Gameplays/FindTheRightAnswers/GameplayFindTheRightAnswers+DragAndDropIntoZones.swift new file mode 100644 index 0000000000..042d68fede --- /dev/null +++ b/Modules/GameEngineKit/Sources/Gameplays/FindTheRightAnswers/GameplayFindTheRightAnswers+DragAndDropIntoZones.swift @@ -0,0 +1,51 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import Foundation + +// MARK: - GameplayDragAndDropIntoZonesChoiceModel + +struct GameplayDragAndDropIntoZonesChoiceModel: GameplayChoiceModelProtocol { + typealias ChoiceType = DragAndDropIntoZones.Choice + + let id: String = UUID().uuidString + let choice: ChoiceType + var state: GameplayChoiceState = .idle +} + +extension GameplayFindTheRightAnswers where ChoiceModelType == GameplayDragAndDropIntoZonesChoiceModel { + convenience init(choices: [GameplayDragAndDropIntoZonesChoiceModel], allowedTrials: Int? = nil) { + self.init() + self.choices.send(choices) + rightAnswers = choices.filter { $0.choice.dropZone != .none } + state.send(.playing) + + if let allowedTrials { + self.allowedTrials = allowedTrials + } else { + self.allowedTrials = getNumberOfAllowedTrials(from: kGradingLUTDefault) + } + } + + func process(_ choice: ChoiceModelType, _ dropZone: DragAndDropIntoZones.DropZone) { + guard rightAnswers.isNotEmpty else { + return + } + + numberOfTrials += 1 + + if choice.choice.dropZone == dropZone { + updateChoice(choice, state: .rightAnswer) + rightAnswers.removeAll { $0.id == choice.id } + } else { + updateChoice(choice, state: .wrongAnswer) + } + + if rightAnswers.isEmpty { + let level = evaluateCompletionLevel(allowedTrials: allowedTrials, numberOfTrials: numberOfTrials) + state.send(.completed(level: level)) + } + } +} diff --git a/Modules/GameEngineKit/Sources/Gameplays/FindTheRightAnswers/GameplayFindTheRightAnswers+TouchToSelect.swift b/Modules/GameEngineKit/Sources/Gameplays/FindTheRightAnswers/GameplayFindTheRightAnswers+TouchToSelect.swift new file mode 100644 index 0000000000..c27055c503 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Gameplays/FindTheRightAnswers/GameplayFindTheRightAnswers+TouchToSelect.swift @@ -0,0 +1,51 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import Foundation + +// MARK: - GameplayTouchToSelectChoiceModel + +struct GameplayTouchToSelectChoiceModel: GameplayChoiceModelProtocol { + typealias ChoiceType = TouchToSelect.Choice + + let id: String = UUID().uuidString + let choice: ChoiceType + var state: GameplayChoiceState = .idle +} + +extension GameplayFindTheRightAnswers where ChoiceModelType == GameplayTouchToSelectChoiceModel { + convenience init(choices: [GameplayTouchToSelectChoiceModel], shuffle: Bool = false, allowedTrials: Int? = nil) { + self.init() + self.choices.send(shuffle ? choices.shuffled() : choices) + rightAnswers = choices.filter(\.choice.isRightAnswer) + state.send(.playing) + + if let allowedTrials { + self.allowedTrials = allowedTrials + } else { + self.allowedTrials = getNumberOfAllowedTrials(from: kGradingLUTDefault) + } + } + + func process(_ choice: ChoiceModelType) { + guard rightAnswers.isNotEmpty else { + return + } + + numberOfTrials += 1 + + if choice.choice.isRightAnswer, rightAnswers.isNotEmpty { + updateChoice(choice, state: .rightAnswer) + rightAnswers.removeAll { $0.id == choice.id } + } else { + updateChoice(choice, state: .wrongAnswer) + } + + if rightAnswers.isEmpty { + let level = evaluateCompletionLevel(allowedTrials: allowedTrials, numberOfTrials: numberOfTrials) + state.send(.completed(level: level)) + } + } +} diff --git a/Modules/GameEngineKit/Sources/Gameplays/FindTheRightAnswers/GameplayFindTheRightAnswers.swift b/Modules/GameEngineKit/Sources/Gameplays/FindTheRightAnswers/GameplayFindTheRightAnswers.swift new file mode 100644 index 0000000000..f494d54d08 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Gameplays/FindTheRightAnswers/GameplayFindTheRightAnswers.swift @@ -0,0 +1,31 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import Foundation + +class GameplayFindTheRightAnswers: StatefulGameplayProtocol + where ChoiceModelType: GameplayChoiceModelProtocol +{ + var choices: CurrentValueSubject<[ChoiceModelType], Never> = .init([]) + var rightAnswers: [ChoiceModelType] = [] + var state: CurrentValueSubject = .init(.idle) + var numberOfTrials: Int = 0 + var allowedTrials: Int = 0 + + func updateChoice(_ choice: ChoiceModelType, state: GameplayChoiceState) { + guard let index = choices.value.firstIndex(where: { $0.id == choice.id }) else { + return + } + self.choices.value[index].state = state + } + + func getNumberOfAllowedTrials(from table: GradingLUT) -> Int { + let numberOfChoices = self.choices.value.count + let numberOfRightAnswers = self.rightAnswers.count + + return table[numberOfChoices]![numberOfRightAnswers]! + } +} diff --git a/Modules/GameEngineKit/Sources/Gameplays/GameplayChoiceModels.swift b/Modules/GameEngineKit/Sources/Gameplays/GameplayChoiceModels.swift new file mode 100644 index 0000000000..29ebc027ed --- /dev/null +++ b/Modules/GameEngineKit/Sources/Gameplays/GameplayChoiceModels.swift @@ -0,0 +1,38 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import Foundation + +// MARK: - kDefaultGradingTable + +typealias GradingLUT = [Int: [Int: Int]] + +// TODO: (@HPezz): Split into several gameplays gradingTables +let kGradingLUTDefault: GradingLUT = [ + 1: [1: 1], + 2: [1: 1, 2: 2], + 3: [1: 1, 2: 2, 3: 3], + 4: [1: 2, 2: 2, 3: 3, 4: 4], + 5: [1: 2, 2: 3, 3: 3, 4: 4, 5: 5], + 6: [1: 3, 2: 3, 3: 4, 4: 4, 5: 5, 6: 6], +] + +// MARK: - GameplayChoiceState + +public enum GameplayChoiceState { + case idle + case rightAnswer + case wrongAnswer +} + +// MARK: - GameplayChoiceModelProtocol + +protocol GameplayChoiceModelProtocol: Identifiable { + associatedtype ChoiceType + + var id: String { get } + var choice: ChoiceType { get } + var state: GameplayChoiceState { get set } // .selected, .idle, .rightAnswer, .wrongAnswer +} diff --git a/Modules/GameEngineKit/Sources/Gameplays/StatefulGameplayProtocol.swift b/Modules/GameEngineKit/Sources/Gameplays/StatefulGameplayProtocol.swift new file mode 100644 index 0000000000..f4146c7318 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Gameplays/StatefulGameplayProtocol.swift @@ -0,0 +1,32 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine + +// MARK: - StatefulGameplayProtocol + +protocol StatefulGameplayProtocol { + var state: CurrentValueSubject { get } + + func evaluateCompletionLevel(allowedTrials: Int, numberOfTrials: Int) -> ExerciseState.CompletionLevel + func getNumberOfAllowedTrials(from table: GradingLUT) -> Int +} + +extension StatefulGameplayProtocol { + func evaluateCompletionLevel(allowedTrials: Int, numberOfTrials: Int) -> ExerciseState.CompletionLevel { + let trialsPercentage = Double(allowedTrials) / Double(numberOfTrials) * 100.0 + switch trialsPercentage { + case 90...: + return .excellent + case 80..<90: + return .good + case 70..<80: + return .average + case 60..<70: + return .belowAverage + default: + return .fail + } + } +} diff --git a/Modules/GameEngineKit/Sources/Utils/AudioPlayer.swift b/Modules/GameEngineKit/Sources/Utils/AudioPlayer.swift new file mode 100644 index 0000000000..1c1847e8e2 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Utils/AudioPlayer.swift @@ -0,0 +1,88 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AVFAudio +import Combine +import ContentKit +import Foundation + +// MARK: - AudioPlayer + +public class AudioPlayer: NSObject, ObservableObject { + // MARK: Lifecycle + + public init(audioRecording: AudioRecording) { + super.init() + self.setAudioPlayer(audioRecording: audioRecording) + self.didFinishPlaying = false + } + + // MARK: Internal + + @Published var progress: CGFloat = 0.0 + @Published var didFinishPlaying = false + + var isPlaying: Bool { + self.player.isPlaying + } + + func setAudioPlayer(audioRecording: AudioRecording) { + self.progress = 0.0 + self.didFinishPlaying = false + + do { + let fileURL = Bundle.module.url(forResource: audioRecording.file, withExtension: "mp3")! + self.player = try AVAudioPlayer(contentsOf: fileURL) + self.player.delegate = self + } catch { + print("ERROR - mp3 file not found - \(error)") + return + } + + self.player.prepareToPlay() + + Timer.publish(every: 0.1, on: .main, in: .default) + .autoconnect() + .receive(on: DispatchQueue.main) + .sink(receiveValue: { _ in + if let player = self.player { + let newProgress = CGFloat(player.currentTime / player.duration) + if self.progress > newProgress { + self.progress = 1.0 + } else { + self.progress = newProgress + } + } + }) + .store(in: &self.cancellables) + } + + func play() { + self.player.play() + self.didFinishPlaying = false + } + + func pause() { + self.player.pause() + } + + func stop() { + self.player.stop() + self.didFinishPlaying = true + } + + // MARK: Private + + private var player: AVAudioPlayer! + + private var cancellables: Set = [] +} + +// MARK: AVAudioPlayerDelegate + +extension AudioPlayer: AVAudioPlayerDelegate { + public func audioPlayerDidFinishPlaying(_: AVAudioPlayer, successfully _: Bool) { + self.didFinishPlaying = true + } +} diff --git a/Modules/GameEngineKit/Sources/Utils/ContinuousProgressBar.swift b/Modules/GameEngineKit/Sources/Utils/ContinuousProgressBar.swift new file mode 100644 index 0000000000..0d3958e0f5 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Utils/ContinuousProgressBar.swift @@ -0,0 +1,32 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct ContinuousProgressBar: View { + let kHeight: CGFloat = 30 + var progress: CGFloat + + var body: some View { + GeometryReader { geometry in + Capsule() + .fill(DesignKitAsset.Colors.progressBar.swiftUIColor) + .frame(height: self.kHeight) + .frame(width: geometry.size.width) + .overlay(alignment: .leading) { + Capsule() + .fill(.green) + .frame(maxWidth: geometry.size.width * self.progress) + .padding(8) + } + .position(x: geometry.frame(in: .local).midX, y: geometry.frame(in: .local).midY) + } + .frame(maxHeight: self.kHeight) + } +} + +#Preview { + ContinuousProgressBar(progress: 0.5) +} diff --git a/Modules/GameEngineKit/Sources/Utils/MIDIInstrument.swift b/Modules/GameEngineKit/Sources/Utils/MIDIInstrument.swift new file mode 100644 index 0000000000..af06d463ff --- /dev/null +++ b/Modules/GameEngineKit/Sources/Utils/MIDIInstrument.swift @@ -0,0 +1,48 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AudioKit + +// MARK: - MIDIInstrument + +enum MIDIInstrument: String { + case xylophone + + // MARK: Internal + + var samples: [MIDISample] { + switch self { + case .xylophone: + xyloSamples + } + } +} + +private let xyloSamples: [MIDISample] = + [ + MIDISample(file: "Xylo-24-C1.wav", note: 24), + MIDISample(file: "Xylo-25-C#1.wav", note: 25), + MIDISample(file: "Xylo-26-D1.wav", note: 26), + MIDISample(file: "Xylo-27-D#1.wav", note: 27), + MIDISample(file: "Xylo-28-E1.wav", note: 28), + MIDISample(file: "Xylo-29-F1.wav", note: 29), + MIDISample(file: "Xylo-30-F#1.wav", note: 30), + MIDISample(file: "Xylo-31-G1.wav", note: 31), + MIDISample(file: "Xylo-32-G#1.wav", note: 32), + MIDISample(file: "Xylo-33-A1.wav", note: 33), + MIDISample(file: "Xylo-34-A#1.wav", note: 34), + MIDISample(file: "Xylo-35-B1.wav", note: 35), + MIDISample(file: "Xylo-36-C2.wav", note: 36), + MIDISample(file: "Xylo-37-C#2.wav", note: 37), + MIDISample(file: "Xylo-38-D2.wav", note: 38), + MIDISample(file: "Xylo-39-D#2.wav", note: 39), + MIDISample(file: "Xylo-40-E2.wav", note: 40), + MIDISample(file: "Xylo-41-F2.wav", note: 41), + MIDISample(file: "Xylo-42-F#2.wav", note: 42), + MIDISample(file: "Xylo-43-G2.wav", note: 43), + MIDISample(file: "Xylo-44-G#2.wav", note: 44), + MIDISample(file: "Xylo-45-A2.wav", note: 45), + MIDISample(file: "Xylo-46-A#2.wav", note: 46), + MIDISample(file: "Xylo-47-B2.wav", note: 47), + ] diff --git a/Modules/GameEngineKit/Sources/Utils/MIDIPlayer.swift b/Modules/GameEngineKit/Sources/Utils/MIDIPlayer.swift new file mode 100644 index 0000000000..dd4bc5891d --- /dev/null +++ b/Modules/GameEngineKit/Sources/Utils/MIDIPlayer.swift @@ -0,0 +1,84 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AudioKit +import AVFAudio +import ContentKit +import SwiftUI + +class MIDIPlayer: ObservableObject { + // MARK: Lifecycle + + init(instrument: MIDIInstrument) { + self.engine.output = self.sampler + + self.loadInstrument(samples: instrument.samples) + self.startAudioEngine() + } + + // MARK: Internal + + func loadMIDIFile(fileURL: URL, tempo: Double) { + self.sequencer.loadMIDIFile(fromURL: fileURL) + self.sequencer.setGlobalMIDIOutput(self.instrument.midiIn) + self.sequencer.setTempo(tempo) + } + + func setInstrumentCallback(callback: @escaping MIDICallback) { + self.instrument.callback = callback + } + + func noteOn(number: MIDINoteNumber, velocity: MIDIVelocity = 60) { + self.sampler.play(noteNumber: number, velocity: velocity, channel: 0) + } + + func play() { + self.sequencer.rewind() + self.sequencer.play() + } + + func stop() { + self.sequencer.stop() + } + + func getMidiNotes() -> [MIDINoteData] { + guard let track = sequencer.tracks.first else { + fatalError("Sequencer track not found") + } + + return track.getMIDINoteData() + } + + func getDuration() -> Double { + guard let track = sequencer.tracks.first else { + fatalError("Sequencer track not found") + } + + return track.length * 60 / self.sequencer.tempo + } + + // MARK: Private + + private let engine = AudioEngine() + private let sampler = MIDISampler() + private let sequencer = AppleSequencer() + private let instrument = MIDICallbackInstrument() + + private func startAudioEngine() { + do { + try self.engine.start() + } catch { + print("Could not start AudioKit") + } + } + + private func loadInstrument(samples: [MIDISample]) { + do { + let files = samples.compactMap(\.audioFile) + try self.sampler.loadAudioFiles(files) + } catch { + print("Could not load file") + } + } +} diff --git a/Modules/GameEngineKit/Sources/Utils/MIDISample.swift b/Modules/GameEngineKit/Sources/Utils/MIDISample.swift new file mode 100644 index 0000000000..c525b0b581 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Utils/MIDISample.swift @@ -0,0 +1,28 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AVFAudio +import SwiftUI + +struct MIDISample { + // MARK: Lifecycle + + init(file: String, note: Int) { + self.fileName = file + self.midiNote = note + + guard let fileURL = Bundle.module.resourceURL?.appendingPathComponent(file) else { return } + do { + self.audioFile = try AVAudioFile(forReading: fileURL) + } catch { + fatalError("Could not load: \(self.fileName)") + } + } + + // MARK: Internal + + var fileName: String + var midiNote: Int + var audioFile: AVAudioFile? +} diff --git a/Modules/GameEngineKit/Sources/Utils/MIDIScale.swift b/Modules/GameEngineKit/Sources/Utils/MIDIScale.swift new file mode 100644 index 0000000000..d0e22c0e56 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Utils/MIDIScale.swift @@ -0,0 +1,21 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AudioKit + +enum MIDIScale: String { + case majorPentatonic + case majorHeptatonic + + // MARK: Internal + + var notes: [MIDINoteNumber] { + switch self { + case .majorPentatonic: + [24, 26, 28, 31, 33] + case .majorHeptatonic: + [24, 26, 28, 29, 31, 33, 35, 36] + } + } +} diff --git a/Modules/GameEngineKit/Sources/Utils/SpriteKitExtensions/SKActionExtension.swift b/Modules/GameEngineKit/Sources/Utils/SpriteKitExtensions/SKActionExtension.swift new file mode 100644 index 0000000000..b2973aca5e --- /dev/null +++ b/Modules/GameEngineKit/Sources/Utils/SpriteKitExtensions/SKActionExtension.swift @@ -0,0 +1,13 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SpriteKit +import SwiftUI + +extension SKAction { + func moveAnimation(_ timingMode: SKActionTimingMode) -> SKAction { + self.timingMode = timingMode + return self + } +} diff --git a/Modules/GameEngineKit/Sources/Utils/SpriteKitExtensions/SKSpriteNodeExtension.swift b/Modules/GameEngineKit/Sources/Utils/SpriteKitExtensions/SKSpriteNodeExtension.swift new file mode 100644 index 0000000000..101d7d6b00 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Utils/SpriteKitExtensions/SKSpriteNodeExtension.swift @@ -0,0 +1,30 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SpriteKit +import SwiftUI + +extension SKSpriteNode { + func fullyContains(bounds: CGRect) -> Bool { + (position.x - (size.width / 2) >= bounds.minX) + && (position.y - (size.height / 2) >= bounds.minY) + && (position.x + (size.width / 2) <= bounds.maxX) + && (position.y + (size.height / 2) <= bounds.maxY) + } + + func fullyContains(location: CGPoint, bounds: CGRect) -> Bool { + (location.x - (size.width / 3) >= bounds.minX) + && (location.y - (size.height / 3) >= bounds.minY) + && (location.x + (size.width / 3) <= bounds.maxX) + && (location.y + (size.height / 3) <= bounds.maxY) + } + + // make sure the bigger side of a node measures a max of 170 pts + func scaleForMax(sizeOf: CGFloat) { + let initialSize = texture?.size() + let biggerSide = max(initialSize!.width, initialSize!.height) + let scaler = sizeOf / biggerSide + setScale(scaler) + } +} diff --git a/Modules/GameEngineKit/Sources/Utils/Utils.swift b/Modules/GameEngineKit/Sources/Utils/Utils.swift new file mode 100644 index 0000000000..92cb8d43e3 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Utils/Utils.swift @@ -0,0 +1,48 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import SwiftUI + +public func convertJoystickPosToSpeed(position: CGPoint, maxValue: CGFloat) -> ( + leftSpeed: CGFloat, leftRight: CGFloat +) { + let posX = position.x + let posY = position.y + + let leftSpeed = (posX - posY) / maxValue + let rightSpeed = -(posX + posY) / maxValue + + let leftSpeedClamped = clamp(leftSpeed, lower: -1.0, upper: 1.0) + let rightSpeedClamped = clamp(rightSpeed, lower: -1.0, upper: 1.0) + + return (leftSpeedClamped, rightSpeedClamped) +} + +func clamp(_ value: T, lower: T, upper: T) -> T { + min(max(value, lower), upper) +} + +extension View { + func onTapGestureIf(_ condition: Bool, closure: @escaping () -> Void) -> some View { + allowsHitTesting(condition) + .onTapGesture { + closure() + } + } +} + +extension Shape { + func fill( + _ fillStyle: some ShapeStyle, strokeBorder strokeStyle: some ShapeStyle, lineWidth: CGFloat = 1 + ) -> some View { + stroke(strokeStyle, lineWidth: lineWidth) + .background(self.fill(fillStyle)) + } +} + +// rotation animations +func degreesToRadian(degrees: Double) -> Double { + Double(degrees / 180.0 * .pi) +} diff --git a/Modules/GameEngineKit/Sources/Views/Activity/ActivityDetailsView.swift b/Modules/GameEngineKit/Sources/Views/Activity/ActivityDetailsView.swift new file mode 100644 index 0000000000..41dee688f4 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Views/Activity/ActivityDetailsView.swift @@ -0,0 +1,120 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import DesignKit +import Fit +import MarkdownUI +import SwiftUI + +// MARK: - ActivityDetailsView + +public struct ActivityDetailsView: View { + // MARK: Lifecycle + + public init(activity: Activity) { + self.activity = activity + } + + // MARK: Public + + public var body: some View { + List { + Section { + HStack { + HStack(alignment: .center) { + Image(uiImage: self.activity.details.iconImage) + .resizable() + .scaledToFit() + .frame(width: 120, height: 120) + .clipShape(Circle()) + + VStack(alignment: .leading, spacing: 8) { + Text(self.activity.details.title) + .font(.largeTitle) + .bold() + + if let subtitle = self.activity.details.subtitle { + Text(subtitle) + .font(.title2) + .foregroundColor(.secondary) + } + + Text(markdown: self.activity.details.shortDescription) + .font(.subheadline) + .foregroundColor(.secondary) + } + } + + Spacer() + } + + HStack(alignment: .firstTextBaseline) { + Text("**Skills**") + Spacer() + Fit(itemSpacing: .viewSpacing(minimum: 15)) { + ForEach(self.activity.skills, id: \.self) { skill in + let skill = Skills.skill(id: skill)! + + TagView(title: skill.name, systemImage: "info.circle") { + self.selectedSkill = skill + } + } + } + .sheet(item: self.$selectedSkill, onDismiss: { self.selectedSkill = nil }, content: { skill in + VStack(alignment: .leading) { + Text(skill.name) + .font(.headline) + Text(skill.description) + } + }) + } + + HStack(alignment: .firstTextBaseline) { + Text("**Authors**") + Spacer() + Fit(itemSpacing: .viewSpacing(minimum: 15)) { + ForEach(self.activity.authors, id: \.self) { author in + let author = Authors.hmi(id: author)! + + TagView(title: author.name, systemImage: "info.circle") { + self.selectedAuthor = author + } + } + } + .sheet(item: self.$selectedAuthor, onDismiss: { self.selectedAuthor = nil }, content: { author in + VStack(alignment: .leading) { + Text(author.name) + .font(.headline) + Text(author.description) + } + }) + } + } + + Section("Description") { + Markdown(self.activity.details.description) + .markdownTheme(.gitHub) + } + + Section("Instructions") { + Markdown(self.activity.details.instructions) + .markdownTheme(.gitHub) + } + } + } + + // MARK: Private + + private let activity: Activity + + @State private var selectedAuthor: Author? + @State private var selectedSkill: Skill? +} + +#Preview { + NavigationStack { + ActivityDetailsView(activity: Activity.mock) + } +} diff --git a/Modules/GameEngineKit/Sources/Views/Activity/ActivityProgressBar.swift b/Modules/GameEngineKit/Sources/Views/Activity/ActivityProgressBar.swift new file mode 100644 index 0000000000..3d991e1504 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Views/Activity/ActivityProgressBar.swift @@ -0,0 +1,89 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct ActivityProgressBar: View { + // MARK: Internal + + @ObservedObject var viewModel: ActivityViewViewModel + + var body: some View { + HStack(spacing: 0) { + Spacer() + ForEach(0.. Color { + switch level { + case .excellent: + .green + case .good: + .orange + case .average, + .belowAverage, + .fail: + .red + case .nonApplicable, + .none: + .gray + } + } + + private func progressBarMarkerColor(group: Int, exercise: Int) -> Color { + if let completedExerciseSharedData = self.viewModel.completedExercisesSharedData.first(where: { + $0.groupIndex == group + && $0.exerciseIndex == exercise + }) { + self.completionLevelToColor(level: completedExerciseSharedData.completionLevel) + } else { + .white + } + } +} diff --git a/Modules/GameEngineKit/Sources/Views/Activity/ActivityProgressBarMarker.swift b/Modules/GameEngineKit/Sources/Views/Activity/ActivityProgressBarMarker.swift new file mode 100644 index 0000000000..e0e28a4143 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Views/Activity/ActivityProgressBarMarker.swift @@ -0,0 +1,24 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct ActivityProgressBarMarker: View { + @Binding var color: Color + @Binding var isCurrentlyPlaying: Bool + + var body: some View { + Circle() + .stroke(Color.white, lineWidth: 3) + .background(self.color, in: Circle()) + .overlay { + Circle() + .fill(DesignKitAsset.Colors.chevron.swiftUIColor) + .padding(4) + .scaleEffect(self.isCurrentlyPlaying ? 1 : 0.01) + .animation(.easeIn(duration: 0.5).delay(0.2), value: self.isCurrentlyPlaying) + } + } +} diff --git a/Modules/GameEngineKit/Sources/Views/Activity/ActivityView+SuccessFailureView+l10n.swift b/Modules/GameEngineKit/Sources/Views/Activity/ActivityView+SuccessFailureView+l10n.swift new file mode 100644 index 0000000000..19263f1bf8 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Views/Activity/ActivityView+SuccessFailureView+l10n.swift @@ -0,0 +1,40 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +// MARK: - l10n.SuccessFailureView + +// swiftlint:disable line_length + +extension l10n { + enum SuccessFailureView { + static let successPercentageLabel = LocalizedStringInterpolation("gameenginekit.success_failure_view.success_percentage_label", + bundle: GameEngineKitResources.bundle, + value: "%.0f%% of success!", + comment: "Success and Failure view success percentage label") + + static let successCheeringLabel = LocalizedString("gameenginekit.success_failure_view.success_cheering_label", + bundle: GameEngineKitResources.bundle, + value: "Well done, you've succeeded this activity!", + comment: "Success and Failure view cheering label") + + static let failureCheeringLabel = LocalizedString("gameenginekit.success_failure_view.failure_cheering_label", + bundle: GameEngineKitResources.bundle, + value: "Try again!", + comment: "Success and Failure view cheering label") + + static let quitWithoutSavingButtonLabel = LocalizedString("gameenginekit.success_failure_view.quit_without_saving_button_label", + bundle: GameEngineKitResources.bundle, + value: "Quit without saving", + comment: "Success and Failure view quit without saving button label") + + static let saveQuitButtonLabel = LocalizedString("gameenginekit.success_failure_view.save_quit_button_label", + bundle: GameEngineKitResources.bundle, + value: "Save", + comment: "Success and Failure view save & quit button label") + } +} + +// swiftlint:enable line_length diff --git a/Modules/GameEngineKit/Sources/Views/Activity/ActivityView+SuccessView.swift b/Modules/GameEngineKit/Sources/Views/Activity/ActivityView+SuccessView.swift new file mode 100644 index 0000000000..01ab775f20 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Views/Activity/ActivityView+SuccessView.swift @@ -0,0 +1,54 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import Lottie +import SwiftUI + +// MARK: - ActivityView.SuccessView + +extension ActivityView { + struct SuccessView: View { + let percentage: Double + + var body: some View { + VStack(spacing: 40) { + VStack { + Text(l10n.SuccessFailureView.successPercentageLabel(self.percentage)) + .font(.largeTitle) + .foregroundStyle(.teal) + .padding(10) + Text(l10n.SuccessFailureView.successCheeringLabel) + .font(.largeTitle) + } + + LottieView( + animation: .bravo, + speed: 0.6 + ) + .frame(height: 500) + .padding(-100) + + HStack(spacing: 40) { + Button(String(l10n.SuccessFailureView.quitWithoutSavingButtonLabel.characters)) { + // TODO: (@mathieu) - Save undisplayable data in session + UIApplication.shared.dismissAll(animated: true) + } + .buttonStyle(.bordered) + + Button(String(l10n.SuccessFailureView.saveQuitButtonLabel.characters)) { + // TODO: (@mathieu) - Save displayable data in session + UIApplication.shared.dismissAll(animated: true) + } + .buttonStyle(.borderedProminent) + } + } + } + } +} + +#Preview { + ActivityView.SuccessView(percentage: 65) +} diff --git a/Modules/GameEngineKit/Sources/Views/Activity/ActivityView.swift b/Modules/GameEngineKit/Sources/Views/Activity/ActivityView.swift new file mode 100644 index 0000000000..1160bba254 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Views/Activity/ActivityView.swift @@ -0,0 +1,301 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftlint:disable cyclomatic_complexity void_function_in_ternary function_body_length + +import ContentKit +import DesignKit +import LocalizationKit +import Lottie +import RobotKit +import SwiftUI + +// MARK: - ActivityView + +public struct ActivityView: View { + // MARK: Lifecycle + + public init(activity: Activity) { + self._viewModel = StateObject(wrappedValue: ActivityViewViewModel(activity: activity)) + } + + // MARK: Public + + public var body: some View { + ZStack(alignment: .bottomTrailing) { + VStack { + VStack(spacing: 15) { + if self.viewModel.isProgressBarVisible { + ActivityProgressBar(viewModel: self.viewModel) + } + + if self.viewModel.isExerciseInstructionsButtonVisible { + ExerciseInstructionsButton(instructions: self.viewModel.currentExercise.instructions!) + } + } + + VStack { + Spacer() + self.currentExerciseInterface() + Spacer() + } + } + .id(self.viewModel.currentExerciseIndexInCurrentGroup) + .blur(radius: self.blurRadius) + .opacity(self.opacity) + .onChange(of: self.viewModel.isReinforcerAnimationVisible) { + if $0 { + withAnimation(.easeInOut.delay(0.5)) { + self.blurRadius = 20 + } + } else { + withAnimation { + self.blurRadius = 0 + } + } + } + .onChange(of: self.viewModel.isCurrentActivityCompleted) { + if $0 { + withAnimation { + self.opacity = 0 + } + } else { + withAnimation { + self.opacity = 1 + } + } + } + + if self.viewModel.isReinforcerAnimationVisible { + self.reinforcerAnimationView + .frame(maxWidth: .infinity) + } + + HStack { + if case .completed = self.viewModel.currentExerciseSharedData.state { + if self.viewModel.isReinforcerAnimationVisible { + self.hideReinforcerToShowAnswersButton + } + self.continueButton + } + } + } + .frame(maxWidth: .infinity) + .background(.lkBackground) + .ignoresSafeArea(.all, edges: .bottom) + .navigationTitle(self.viewModel.currentActivity.details.title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button { + self.isAlertPresented = true + } label: { + Image(systemName: "xmark.circle") + } + } + ToolbarItem(placement: .topBarTrailing) { + Button { + self.isInfoSheetPresented.toggle() + } label: { + Image(systemName: "info.circle") + } + } + } + .alert(String(l10n.GameEngineKit.ActivityView.QuitActivityAlert.title.characters), isPresented: self.$isAlertPresented) { + Button(String(l10n.GameEngineKit.ActivityView.QuitActivityAlert.saveQuitButtonLabel.characters), action: { + // TODO: (@mathieu) - Save displayable data in session + self.dismiss() + }) + Button(String(l10n.GameEngineKit.ActivityView.QuitActivityAlert.quitWithoutSavingButtonLabel.characters), role: .destructive, action: { + // TODO: (@mathieu) - Save undisplayable data in session + self.dismiss() + }) + Button(String(l10n.GameEngineKit.ActivityView.QuitActivityAlert.cancelButtonLabel.characters), role: .cancel, action: { + self.isAlertPresented = false + }) + } message: { + Text(l10n.GameEngineKit.ActivityView.QuitActivityAlert.message) + } + .sheet(isPresented: self.$isInfoSheetPresented) { + ActivityDetailsView(activity: self.viewModel.currentActivity) + } + .fullScreenCover(isPresented: self.$viewModel.isCurrentActivityCompleted) { + self.endOfActivityScoreView + } + .onAppear { + Robot.shared.stop() + UIApplication.shared.isIdleTimerDisabled = true + } + .onDisappear { + Robot.shared.stop() + UIApplication.shared.isIdleTimerDisabled = false + } + } + + // MARK: Internal + + @Environment(\.dismiss) var dismiss + + // TODO: (@ladislas) was @ObservedObject, check why + @StateObject var viewModel: ActivityViewViewModel + + // MARK: Private + + @State private var isAlertPresented: Bool = false + + @State private var opacity: Double = 1 + @State private var blurRadius: CGFloat = 0 + @State private var showScoreView: Bool = false + @State private var isInfoSheetPresented: Bool = false + + private let robot = Robot.shared + + @ViewBuilder + private var endOfActivityScoreView: some View { + if self.viewModel.didCompleteActivitySuccessfully { + SuccessView(percentage: self.viewModel.activityCompletionSuccessPercentage) + } else { + FailureView(percentage: self.viewModel.activityCompletionSuccessPercentage) + } + } + + @ViewBuilder + private var reinforcerAnimationView: some View { + LottieView( + animation: .reinforcer, + speed: 0.2 + ) + .onAppear { + // TODO(@ladislas/@hugo): Use reinforcer children choice + self.robot.run(.fire) + } + .transition( + .asymmetric( + insertion: .opacity.animation(.snappy.delay(0.75)), + removal: .identity + ) + ) + } + + @ViewBuilder + private var continueButton: some View { + Button(String(l10n.GameEngineKit.ActivityView.continueButton.characters)) { + if self.viewModel.isLastExercise { + self.viewModel.scorePanelEnabled ? self.viewModel.moveToActivityEnd() : self.dismiss() + } else { + self.viewModel.moveToNextExercise() + } + } + .buttonStyle(.borderedProminent) + .tint(.green) + .padding() + .transition( + .asymmetric( + insertion: .opacity.animation(.snappy.delay(self.viewModel.delayAfterReinforcerAnimation)), + removal: .identity + ) + ) + } + + @ViewBuilder + private var hideReinforcerToShowAnswersButton: some View { + Button(String(l10n.GameEngineKit.ActivityView.hideReinforcerToShowAnswersButton.characters)) { + withAnimation { + self.viewModel.isReinforcerAnimationVisible = false + } + } + .buttonStyle(.bordered) + .padding() + .transition( + .asymmetric( + insertion: .opacity.animation(.snappy.delay(self.viewModel.delayAfterReinforcerAnimation)), + removal: .identity + ) + ) + } + + @ViewBuilder + private func currentExerciseInterface() -> some View { + switch self.viewModel.currentExerciseInterface { + case .touchToSelect: + TouchToSelectView( + exercise: self.viewModel.currentExercise, + data: self.viewModel.currentExerciseSharedData + ) + + case .robotThenTouchToSelect: + RobotThenTouchToSelectView( + exercise: self.viewModel.currentExercise, + data: self.viewModel.currentExerciseSharedData + ) + + case .listenThenTouchToSelect: + ListenThenTouchToSelectView( + exercise: self.viewModel.currentExercise, + data: self.viewModel.currentExerciseSharedData + ) + + case .observeThenTouchToSelect: + ObserveThenTouchToSelectView( + exercise: self.viewModel.currentExercise, + data: self.viewModel.currentExerciseSharedData + ) + + case .dragAndDropIntoZones: + DragAndDropIntoZonesView( + exercise: self.viewModel.currentExercise, + data: self.viewModel.currentExerciseSharedData + ) + + case .dragAndDropToAssociate: + DragAndDropToAssociateView( + exercise: self.viewModel.currentExercise, + data: self.viewModel.currentExerciseSharedData + ) + + case .danceFreeze: + DanceFreezeView( + exercise: self.viewModel.currentExercise, + data: self.viewModel.currentExerciseSharedData + ) + + case .remoteStandard: + RemoteStandard.MainView() + + case .remoteArrow: + RemoteArrowView() + + case .hideAndSeek: + HideAndSeekView( + exercise: self.viewModel.currentExercise, + data: self.viewModel.currentExerciseSharedData + ) + + case .musicalInstruments: + MusicalInstrumentView( + exercise: self.viewModel.currentExercise, + data: self.viewModel.currentExerciseSharedData + ) + + case .melody: + MelodyView( + exercise: self.viewModel.currentExercise, + data: self.viewModel.currentExerciseSharedData + ) + + case .pairing: + DiscoverLekaView( + data: self.viewModel.currentExerciseSharedData + ) + } + } +} + +// swiftlint:enable cyclomatic_complexity void_function_in_ternary function_body_length + +#Preview { + NavigationStack { + ActivityView(activity: Activity.mock) + } +} diff --git a/Modules/GameEngineKit/Sources/Views/Activity/ActivityView.swift+l10n.swift b/Modules/GameEngineKit/Sources/Views/Activity/ActivityView.swift+l10n.swift new file mode 100644 index 0000000000..c419beb46a --- /dev/null +++ b/Modules/GameEngineKit/Sources/Views/Activity/ActivityView.swift+l10n.swift @@ -0,0 +1,67 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +// swiftlint:disable nesting line_length + +extension l10n { + enum GameEngineKit { + enum ActivityView { + enum QuitActivityAlert { + static let title = LocalizedString("gameenginekit.activity_view.quit_activity_alert.title", + bundle: GameEngineKitResources.bundle, + value: "Quit activity?", + comment: "Quit activity alert title") + + static let message = LocalizedString("gameenginekit.activity_view.quit_activity_alert.message", + bundle: GameEngineKitResources.bundle, + value: """ + Do you want to save your progress before quitting? + """, + comment: "Quit activity alert message") + + static let quitWithoutSavingButtonLabel = LocalizedString("gameenginekit.activity_view.quit_activity_alert.quit_without_saving_button_label", + bundle: GameEngineKitResources.bundle, + value: "Quit Without Saving", + comment: "Quit activity alert quit without saving button label") + + static let saveQuitButtonLabel = LocalizedString("gameenginekit.activity_view.quit_activity_alert.save_quit_button_label", + bundle: GameEngineKitResources.bundle, + value: "Save and Quit", + comment: "Quit activity alert save and quit button label") + + static let cancelButtonLabel = LocalizedString("gameenginekit.activity_view.quit_activity_alert.cancel_button_label", + bundle: GameEngineKitResources.bundle, + value: "Cancel", + comment: "Quit activity alert cancel button label") + } + + enum Toolbar { + static let dismissButton = LocalizedString( + "gameenginekit.activity_view.toolbar.dismiss_button", + bundle: GameEngineKitResources.bundle, + value: "Dismiss", + comment: "The title of the dismiss button" + ) + } + + static let continueButton = LocalizedString( + "gameenginekit.activity_view.continue_button", + bundle: GameEngineKitResources.bundle, + value: "Continue", + comment: "The title of the continue button" + ) + + static let hideReinforcerToShowAnswersButton = LocalizedString( + "gameenginekit.activity_view.hide_reinforcer_to_show_answers_button", + bundle: GameEngineKitResources.bundle, + value: "Review answers", + comment: "The title of the hide reinforcer to show answers button" + ) + } + } +} + +// swiftlint:enable nesting line_length diff --git a/Modules/GameEngineKit/Sources/Views/Activity/ActivityViewViewModel.swift b/Modules/GameEngineKit/Sources/Views/Activity/ActivityViewViewModel.swift new file mode 100644 index 0000000000..66fc6b0880 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Views/Activity/ActivityViewViewModel.swift @@ -0,0 +1,155 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import ContentKit +import SwiftUI + +class ActivityViewViewModel: ObservableObject { + // MARK: Lifecycle + + init(activity: Activity) { + self.currentActivity = activity + + self.activityManager = CurrentActivityManager(activity: activity) + + self.totalGroups = self.activityManager.totalGroups + self.currentGroupIndex = self.activityManager.currentGroupIndex + + self.groupSizeEnumeration = self.activityManager.activity.exercisePayload.exerciseGroups.map(\.exercises.count) + + self.totalExercisesInCurrentGroup = self.activityManager.totalExercisesInCurrentGroup + self.currentExerciseIndexInCurrentGroup = self.activityManager.currentExerciseIndexInCurrentGroup + + self.currentExercise = self.activityManager.currentExercise + self.currentExerciseInterface = self.activityManager.currentExercise.interface + + self.currentExerciseSharedData = ExerciseSharedData( + groupIndex: self.activityManager.currentGroupIndex, + exerciseIndex: self.activityManager.currentExerciseIndexInCurrentGroup + ) + self.completedExercisesSharedData.append(self.currentExerciseSharedData) + + self.subscribeToCurrentExerciseSharedDataUpdates() + } + + // MARK: Internal + + @Published var currentActivity: Activity + + @Published var totalGroups: Int + @Published var currentGroupIndex: Int + @Published var groupSizeEnumeration: [Int] + + @Published var totalExercisesInCurrentGroup: Int + @Published var currentExerciseIndexInCurrentGroup: Int + + @Published var currentExercise: Exercise + @Published var currentExerciseInterface: Exercise.Interface + + @Published var completedExercisesSharedData: [ExerciseSharedData] = [] + @Published var currentExerciseSharedData: ExerciseSharedData + + @Published var isCurrentActivityCompleted: Bool = false + @Published var isReinforcerAnimationVisible: Bool = false + @Published var isReinforcerAnimationEnabled: Bool = true + + var successExercisesSharedData: [ExerciseSharedData] { + self.completedExercisesSharedData.filter { + $0.completionLevel == .excellent + || $0.completionLevel == .good + } + } + + var didCompleteActivitySuccessfully: Bool { + let minimalSuccessPercentage = 0.8 + + return Double(self.successExercisesSharedData.count) > (Double(self.completedExercisesSharedData.count) * minimalSuccessPercentage) + } + + var scorePanelEnabled: Bool { + !self.completedExercisesSharedData.filter { + $0.completionLevel != .nonApplicable + }.isEmpty + } + + var activityCompletionSuccessPercentage: Double { + let successfulExercises = Double(self.successExercisesSharedData.count) + let totalExercises = Double(self.completedExercisesSharedData.filter { + $0.completionLevel != .nonApplicable + }.count) + + return (successfulExercises / totalExercises) * 100.0 + } + + var delayAfterReinforcerAnimation: Double { + self.isReinforcerAnimationEnabled ? 5 : 0.5 + } + + var isProgressBarVisible: Bool { + self.totalGroups > 1 || self.totalExercisesInCurrentGroup != 1 + } + + var isExerciseInstructionsButtonVisible: Bool { + guard let instructions = self.currentExercise.instructions else { return false } + return !instructions.isEmpty + } + + var isFirstExercise: Bool { + self.activityManager.isFirstExercise + } + + var isLastExercise: Bool { + self.activityManager.isLastExercise + } + + func moveToNextExercise() { + self.activityManager.moveToNextExercise() + self.updateValues() + } + + func moveToPreviousExercise() { + self.activityManager.moveToPreviousExercise() + self.updateValues() + } + + func moveToActivityEnd() { + self.isCurrentActivityCompleted = true + } + + // MARK: Private + + private let activityManager: CurrentActivityManager + + private var cancellables: Set = [] + + private func updateValues() { + self.currentExercise = self.activityManager.currentExercise + self.currentExerciseInterface = self.activityManager.currentExercise.interface + self.currentGroupIndex = self.activityManager.currentGroupIndex + self.totalGroups = self.activityManager.totalGroups + self.currentExerciseIndexInCurrentGroup = self.activityManager.currentExerciseIndexInCurrentGroup + self.totalExercisesInCurrentGroup = self.activityManager.totalExercisesInCurrentGroup + self.currentExerciseSharedData = ExerciseSharedData( + groupIndex: self.activityManager.currentGroupIndex, + exerciseIndex: self.activityManager.currentExerciseIndexInCurrentGroup + ) + self.completedExercisesSharedData.append(self.currentExerciseSharedData) + + self.subscribeToCurrentExerciseSharedDataUpdates() + } + + private func subscribeToCurrentExerciseSharedDataUpdates() { + self.currentExerciseSharedData.objectWillChange + .receive(on: DispatchQueue.main) + .sink { + if self.isReinforcerAnimationEnabled, case .completed = self.currentExerciseSharedData.state { + self.isReinforcerAnimationVisible = true + } else { + self.isReinforcerAnimationVisible = false + } + } + .store(in: &self.cancellables) + } +} diff --git a/Modules/GameEngineKit/Sources/Views/Activity/Activityview+FailureView.swift b/Modules/GameEngineKit/Sources/Views/Activity/Activityview+FailureView.swift new file mode 100644 index 0000000000..f3480f0405 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Views/Activity/Activityview+FailureView.swift @@ -0,0 +1,54 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import LocalizationKit +import Lottie +import SwiftUI + +// MARK: - ActivityView.FailureView + +extension ActivityView { + struct FailureView: View { + let percentage: Double + + var body: some View { + VStack(spacing: 40) { + VStack { + Text(l10n.SuccessFailureView.successPercentageLabel(self.percentage)) + .font(.largeTitle) + .foregroundStyle(.teal) + .padding(10) + Text(l10n.SuccessFailureView.failureCheeringLabel) + .font(.largeTitle) + } + + LottieView( + animation: .tryAgain, + speed: 0.6 + ) + .frame(height: 500) + .padding(-100) + + HStack(spacing: 40) { + Button(String(l10n.SuccessFailureView.quitWithoutSavingButtonLabel.characters)) { + // TODO: (@mathieu) - Save undisplayable data in session + UIApplication.shared.dismissAll(animated: true) + } + .buttonStyle(.bordered) + + Button(String(l10n.SuccessFailureView.saveQuitButtonLabel.characters)) { + // TODO: (@mathieu) - Save displayable data in session + UIApplication.shared.dismissAll(animated: true) + } + .buttonStyle(.borderedProminent) + } + } + } + } +} + +#Preview { + ActivityView.FailureView(percentage: 6) +} diff --git a/Modules/GameEngineKit/Sources/Views/Activity/LottieAnimation+Reinforcers.swift b/Modules/GameEngineKit/Sources/Views/Activity/LottieAnimation+Reinforcers.swift new file mode 100644 index 0000000000..82640332fc --- /dev/null +++ b/Modules/GameEngineKit/Sources/Views/Activity/LottieAnimation+Reinforcers.swift @@ -0,0 +1,19 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Lottie + +extension LottieAnimation { + static var reinforcer: LottieAnimation { + LottieAnimation.named("reinforcer_spin_blink.animation.lottie", bundle: .module)! + } + + static var bravo: LottieAnimation { + LottieAnimation.named("activity_end_success.animation.lottie", bundle: .module)! + } + + static var tryAgain: LottieAnimation { + LottieAnimation.named("activity_end_try_again.animation.lottie", bundle: .module)! + } +} diff --git a/Modules/GameEngineKit/Sources/Views/Buttons/XylophoneTileButtonStyle.swift b/Modules/GameEngineKit/Sources/Views/Buttons/XylophoneTileButtonStyle.swift new file mode 100644 index 0000000000..1a05e953a1 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Views/Buttons/XylophoneTileButtonStyle.swift @@ -0,0 +1,90 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct XylophoneTileButtonStyle: ButtonStyle { + // MARK: Lifecycle + + init(index: Int, tileNumber: Int, tileWidth: CGFloat = 100, isTappable: Bool = true) { + self.index = index + self.tileNumber = tileNumber + self.tileWidth = tileWidth + self.isTappable = isTappable + } + + // MARK: Internal + + let xyloAttachColor = Color(red: 0.87, green: 0.65, blue: 0.54) + let defaultMaxTileHeight: Int = 500 + let defaultTileHeightGap: Int = 250 + let defaultTilesScaleFeedback: CGFloat = 0.98 + let defaultTilesRotationFeedback: CGFloat = -1 + + let index: Int + let tileNumber: Int + let tileWidth: CGFloat + let isTappable: Bool + + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + .overlay { + VStack { + Spacer() + Circle() + .fill(self.xyloAttachColor) + .shadow( + color: .black.opacity(0.4), + radius: 3, x: 0, y: 3 + ) + Spacer() + Circle() + .fill(self.xyloAttachColor) + .shadow( + color: self.isTappable ? .black.opacity(0.4) : .clear, + radius: 3, x: 0, y: 3 + ) + Spacer() + } + .frame(width: 44) + } + .overlay { + RoundedRectangle(cornerRadius: 7, style: .circular) + .stroke(.black.opacity(configuration.isPressed ? 0.3 : 0), lineWidth: 20) + } + .clipShape(RoundedRectangle(cornerRadius: 7, style: .circular)) + .frame(width: self.tileWidth, height: self.setSizeFromIndex()) + .scaleEffect( + configuration.isPressed ? self.defaultTilesScaleFeedback : 1, + anchor: .center + ) + .rotationEffect( + Angle(degrees: configuration.isPressed ? self.defaultTilesRotationFeedback : 0), + anchor: .center + ) + .shadow( + color: self.isTappable ? .black.opacity(0.4) : .clear, + radius: 3, x: 0, y: 3 + ) + } + + // MARK: Private + + private func setSizeFromIndex() -> CGFloat { + let sizeDiff = self.defaultTileHeightGap / self.tileNumber + let tileHeight = self.defaultMaxTileHeight - self.index * sizeDiff + + return CGFloat(tileHeight) + } +} + +#Preview { + Button { + // Nothing to do + } label: { + Color(.red) + } + .buttonStyle(XylophoneTileButtonStyle(index: 0, tileNumber: 1, tileWidth: 130)) +} diff --git a/Modules/GameEngineKit/Sources/Views/Exercise/ExerciseInstructionsButton.swift b/Modules/GameEngineKit/Sources/Views/Exercise/ExerciseInstructionsButton.swift new file mode 100644 index 0000000000..7d50ddff5b --- /dev/null +++ b/Modules/GameEngineKit/Sources/Views/Exercise/ExerciseInstructionsButton.swift @@ -0,0 +1,167 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AVFoundation +import DesignKit +import LocalizationKit +import SwiftUI + +// MARK: - SpeakerViewModel + +// TODO(@ladislas): refactor speech synth into own class +class SpeakerViewModel: NSObject, ObservableObject, AVSpeechSynthesizerDelegate { + // MARK: Lifecycle + + override init() { + super.init() + self.synthesizer.delegate = self + } + + deinit { + synthesizer.delegate = nil + } + + // MARK: Internal + + @Published var isSpeaking = false + + func speak(sentence: String) { + let voice = switch l10n.language { + case .french: + AVSpeechSynthesisVoice(language: "fr-FR") + case .english: + AVSpeechSynthesisVoice(language: "en-US") + default: + AVSpeechSynthesisVoice(language: "en-US") + } + + var finalSentence: String { + if l10n.language == .french { + sentence.replacingOccurrences(of: "Leka", with: "Léka") + } else { + sentence + } + } + + var utterance: AVSpeechUtterance { + let utterance = AVSpeechUtterance(string: finalSentence) + utterance.rate = 0.40 + utterance.voice = voice + return utterance + } + + self.synthesizer.speak(utterance) + } + + // MARK: AVSpeechSynthesizerDelegate + + func speechSynthesizer(_: AVSpeechSynthesizer, didStart _: AVSpeechUtterance) { + self.isSpeaking = true + } + + func speechSynthesizer(_: AVSpeechSynthesizer, didFinish _: AVSpeechUtterance) { + self.isSpeaking = false + } + + // MARK: Private + + private var synthesizer = AVSpeechSynthesizer() +} + +// MARK: - ExerciseInstructionsButton + +struct ExerciseInstructionsButton: View { + @StateObject var speaker = SpeakerViewModel() + @State var instructions: String + + var body: some View { + Button(self.instructions) { + self.speaker.speak(sentence: self.instructions) + } + .buttonStyle(StepInstructions_ButtonStyle(isSpeaking: self.$speaker.isSpeaking)) + } +} + +// MARK: - StepInstructions_ButtonStyle + +struct StepInstructions_ButtonStyle: ButtonStyle { + // MARK: Internal + + @Binding var isSpeaking: Bool + + func makeBody(configuration: Self.Configuration) -> some View { + HStack(spacing: 0) { + Spacer() + configuration.label + .foregroundColor(DesignKitAsset.Colors.darkGray.swiftUIColor) + .font(.system(size: 22, weight: .regular)) + .multilineTextAlignment(.center) + .padding(.horizontal, 85) + Spacer() + } + .frame(maxWidth: 640) + .frame(height: 85, alignment: .center) + .background(self.backgroundGradient) + .overlay(self.buttonStroke) + .overlay(self.speachIndicator) + .clipShape( + RoundedRectangle( + cornerRadius: 10, + style: .circular + ) + ) + .shadow( + color: .black.opacity(0.1), + radius: self.isSpeaking ? 0 : 4, x: 0, y: self.isSpeaking ? 1 : 4 + ) + .scaleEffect(self.isSpeaking ? 0.98 : 1) + .disabled(self.isSpeaking) + .animation(.easeOut(duration: 0.2), value: self.isSpeaking) + } + + // MARK: Private + + private var backgroundGradient: some View { + ZStack { + Color.white + LinearGradient( + gradient: Gradient(colors: [.black.opacity(0.1), .black.opacity(0.0), .black.opacity(0.0)]), + startPoint: .top, endPoint: .center + ) + .opacity(self.isSpeaking ? 1 : 0) + } + } + + private var buttonStroke: some View { + RoundedRectangle( + cornerRadius: 10, + style: .circular + ) + .fill( + .clear, + strokeBorder: LinearGradient( + gradient: Gradient(colors: [.black.opacity(0.2), .black.opacity(0.05)]), + startPoint: .bottom, + endPoint: .top + ), + lineWidth: 4 + ) + .opacity(self.isSpeaking ? 0.5 : 0) + } + + private var speachIndicator: some View { + HStack { + Spacer() + DesignKitAsset.Images.personTalking.swiftUIImage + .resizable() + .renderingMode(.template) + .aspectRatio(contentMode: .fit) + .foregroundColor( + self.isSpeaking + ? DesignKitAsset.Colors.lekaDarkBlue.swiftUIColor : DesignKitAsset.Colors.darkGray.swiftUIColor + ) + .padding(10) + } + } +} diff --git a/Modules/GameEngineKit/Sources/Views/Feedback/RightAnswerFeedback.swift b/Modules/GameEngineKit/Sources/Views/Feedback/RightAnswerFeedback.swift new file mode 100644 index 0000000000..72b74dcee0 --- /dev/null +++ b/Modules/GameEngineKit/Sources/Views/Feedback/RightAnswerFeedback.swift @@ -0,0 +1,23 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +public struct RightAnswerFeedback: View { + public var animationPercent: CGFloat + + public var body: some View { + Circle() + .trim(from: 0, to: self.animationPercent) + .stroke( + .green, + style: StrokeStyle( + lineWidth: 6, + lineCap: .round, + lineJoin: .round, + miterLimit: 10 + ) + ) + } +} diff --git a/Modules/GameEngineKit/Sources/Views/Feedback/WrongAnswerFeedback.swift b/Modules/GameEngineKit/Sources/Views/Feedback/WrongAnswerFeedback.swift new file mode 100644 index 0000000000..2c2bc300bd --- /dev/null +++ b/Modules/GameEngineKit/Sources/Views/Feedback/WrongAnswerFeedback.swift @@ -0,0 +1,15 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +public struct WrongAnswerFeedback: View { + public var overlayOpacity: CGFloat + + public var body: some View { + Circle() + .fill(.gray) + .opacity(self.overlayOpacity) + } +} diff --git a/Modules/GameEngineKit/Tests/GameEngineKit_Tests.swift b/Modules/GameEngineKit/Tests/GameEngineKit_Tests.swift new file mode 100644 index 0000000000..3a28a7c40b --- /dev/null +++ b/Modules/GameEngineKit/Tests/GameEngineKit_Tests.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +final class GameEngineKit_Tests: XCTestCase { + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/Modules/GameEngineKit/Tests/Utils_Tests.swift b/Modules/GameEngineKit/Tests/Utils_Tests.swift new file mode 100644 index 0000000000..ffd52f8379 --- /dev/null +++ b/Modules/GameEngineKit/Tests/Utils_Tests.swift @@ -0,0 +1,233 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +@testable import GameEngineKit + +final class UtilsLeftPWMConversion_Tests: XCTestCase { + let maxValue: CGFloat = 300 + + func test_shouldReturnRotations_equalsClockwise_still_still() { + // Given + let position = CGPoint(x: 0.0, y: 0.0) + + // When + let (actualLeftSpeed, actualRightSpeed) = convertJoystickPosToSpeed( + position: position, maxValue: maxValue + ) + + // Then + let expectedLeftSpeed: CGFloat = 0 + let expectedRightSpeed: CGFloat = 0 + + XCTAssertEqual(expectedLeftSpeed, actualLeftSpeed) + XCTAssertEqual(expectedRightSpeed, actualRightSpeed) + } + + func test_shouldReturnRotations_equals_clockwise255_clockwise255() { + // Given + let position = CGPoint(x: 0.0, y: -self.maxValue) + + // When + let (actualLeftSpeed, actualRightSpeed) = convertJoystickPosToSpeed( + position: position, maxValue: maxValue + ) + + // Then + let expectedLeftSpeed: CGFloat = 1 + let expectedRightSpeed: CGFloat = 1 + + XCTAssertEqual(expectedLeftSpeed, actualLeftSpeed) + XCTAssertEqual(expectedRightSpeed, actualRightSpeed) + } + + func test_shouldReturnRotations_equals_counterclockwise255_counterclockwise255() { + // Given + let position = CGPoint(x: 0.0, y: maxValue) + + // When + let (actualLeftSpeed, actualRightSpeed) = convertJoystickPosToSpeed( + position: position, maxValue: maxValue + ) + + // Then + let expectedLeftSpeed: CGFloat = -1 + let expectedRightSpeed: CGFloat = -1 + + XCTAssertEqual(expectedLeftSpeed, actualLeftSpeed) + XCTAssertEqual(expectedRightSpeed, actualRightSpeed) + } + + func test_shouldReturnRotations_equals_clockwise255_counterclockwise255() { + // Given + let position = CGPoint(x: maxValue, y: 0.0) + + // When + let (actualLeftSpeed, actualRightSpeed) = convertJoystickPosToSpeed( + position: position, maxValue: maxValue + ) + + // Then + let expectedLeftSpeed: CGFloat = 1 + let expectedRightSpeed: CGFloat = -1 + + XCTAssertEqual(expectedLeftSpeed, actualLeftSpeed) + XCTAssertEqual(expectedRightSpeed, actualRightSpeed) + } + + func test_shouldReturnRotations_equals_counterclockwise255_clockwise255() { + // Given + let position = CGPoint(x: -self.maxValue, y: 0.0) + + // When + let (actualLeftSpeed, actualRightSpeed) = convertJoystickPosToSpeed( + position: position, maxValue: maxValue + ) + + // Then + let expectedLeftSpeed: CGFloat = -1 + let expectedRightSpeed: CGFloat = 1 + + XCTAssertEqual(expectedLeftSpeed, actualLeftSpeed) + XCTAssertEqual(expectedRightSpeed, actualRightSpeed) + } + + func test_shouldReturnRotations_equals_clockwise127_clockwise127() { + // Given + let position = CGPoint(x: 0.0, y: -self.maxValue / 2) + + // When + let (actualLeftSpeed, actualRightSpeed) = convertJoystickPosToSpeed( + position: position, maxValue: maxValue + ) + + // Then + let expectedLeftSpeed: CGFloat = 0.5 + let expectedRightSpeed: CGFloat = 0.5 + + XCTAssertEqual(expectedLeftSpeed, actualLeftSpeed) + XCTAssertEqual(expectedRightSpeed, actualRightSpeed) + } + + func test_shouldReturnRotations_equals_counterclockwise127_counterclockwise127() { + // Given + let position = CGPoint(x: 0.0, y: maxValue / 2) + + // When + let (actualLeftSpeed, actualRightSpeed) = convertJoystickPosToSpeed( + position: position, maxValue: maxValue + ) + + // Then + let expectedLeftSpeed: CGFloat = -0.5 + let expectedRightSpeed: CGFloat = -0.5 + + XCTAssertEqual(expectedLeftSpeed, actualLeftSpeed) + XCTAssertEqual(expectedRightSpeed, actualRightSpeed) + } + + func test_shouldReturnRotations_equals_clockwise127_counterclockwise127() { + // Given + let position = CGPoint(x: maxValue / 2, y: 0.0) + + // When + let (actualLeftSpeed, actualRightSpeed) = convertJoystickPosToSpeed( + position: position, maxValue: maxValue + ) + + // Then + let expectedLeftSpeed: CGFloat = 0.5 + let expectedRightSpeed: CGFloat = -0.5 + + XCTAssertEqual(expectedLeftSpeed, actualLeftSpeed) + XCTAssertEqual(expectedRightSpeed, actualRightSpeed) + } + + func test_shouldReturnRotations_equals_counterclockwise127_clockwise127() { + // Given + let position = CGPoint(x: -self.maxValue / 2, y: 0.0) + + // When + let (actualLeftSpeed, actualRightSpeed) = convertJoystickPosToSpeed( + position: position, maxValue: maxValue + ) + + // Then + let expectedLeftSpeed: CGFloat = -0.5 + let expectedRightSpeed: CGFloat = 0.5 + + XCTAssertEqual(expectedLeftSpeed, actualLeftSpeed) + XCTAssertEqual(expectedRightSpeed, actualRightSpeed) + } + + func test_shouldReturnRotations_equalsClockwise_still_counterclockwise255() { + // Given + let position = CGPoint(x: maxValue / 2, y: maxValue / 2) + + // When + let (actualLeftSpeed, actualRightSpeed) = convertJoystickPosToSpeed( + position: position, maxValue: maxValue + ) + + // Then + let expectedLeftSpeed: CGFloat = 0 + let expectedRightSpeed: CGFloat = -1 + + XCTAssertEqual(expectedLeftSpeed, actualLeftSpeed) + XCTAssertEqual(expectedRightSpeed, actualRightSpeed) + } + + func test_shouldReturnRotations_equalsClockwise_clockwise255_still() { + // Given + let position = CGPoint(x: maxValue / 2, y: -self.maxValue / 2) + + // When + let (actualLeftSpeed, actualRightSpeed) = convertJoystickPosToSpeed( + position: position, maxValue: maxValue + ) + + // Then + let expectedLeftSpeed: CGFloat = 1 + let expectedRightSpeed: CGFloat = 0 + + XCTAssertEqual(expectedLeftSpeed, actualLeftSpeed) + XCTAssertEqual(expectedRightSpeed, actualRightSpeed) + } + + func test_shouldReturnRotations_equalsCounterclockwise_still_clockwise255() { + // Given + let position = CGPoint(x: -self.maxValue / 2, y: -self.maxValue / 2) + + // When + let (actualLeftSpeed, actualRightSpeed) = convertJoystickPosToSpeed( + position: position, maxValue: maxValue + ) + + // Then + let expectedLeftSpeed: CGFloat = 0 + let expectedRightSpeed: CGFloat = 1 + + XCTAssertEqual(expectedLeftSpeed, actualLeftSpeed) + XCTAssertEqual(expectedRightSpeed, actualRightSpeed) + } + + func test_shouldReturnRotations_equalsCounterclockwise_counterclockwise255_still() { + // Given + let position = CGPoint(x: -self.maxValue / 2, y: self.maxValue / 2) + + // When + let (actualLeftSpeed, actualRightSpeed) = convertJoystickPosToSpeed( + position: position, maxValue: maxValue + ) + + // Then + let expectedLeftSpeed: CGFloat = -1 + let expectedRightSpeed: CGFloat = 0 + + XCTAssertEqual(expectedLeftSpeed, actualLeftSpeed) + XCTAssertEqual(expectedRightSpeed, actualRightSpeed) + } +} diff --git a/Modules/LocalizationKit/Examples/LocalizationKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Modules/LocalizationKit/Examples/LocalizationKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Modules/LocalizationKit/Examples/LocalizationKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/LocalizationKit/Examples/LocalizationKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Modules/LocalizationKit/Examples/LocalizationKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Modules/LocalizationKit/Examples/LocalizationKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/LocalizationKit/Examples/LocalizationKitExample/Resources/Assets.xcassets/Contents.json b/Modules/LocalizationKit/Examples/LocalizationKitExample/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/LocalizationKit/Examples/LocalizationKitExample/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/LocalizationKit/Examples/LocalizationKitExample/Resources/Localizable.xcstrings b/Modules/LocalizationKit/Examples/LocalizationKitExample/Resources/Localizable.xcstrings new file mode 100644 index 0000000000..4bc73f97e0 --- /dev/null +++ b/Modules/LocalizationKit/Examples/LocalizationKitExample/Resources/Localizable.xcstrings @@ -0,0 +1,90 @@ +{ + "version": "1.0", + "sourceLanguage": "en", + "strings": { + "localized_string_NO_default": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "\ud83c\uddfa\ud83c\uddf8 ADDED localized_string_NO_default" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\ud83c\uddeb\ud83c\uddf7 ADDED localized_string_NO_default" + } + } + } + }, + "localized_string_WITH_default": { + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "\u26f3 DEFAULT localized_string_WITH_default" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\ud83c\uddeb\ud83c\uddf7 ADDED localized_string_WITH_default" + } + } + } + }, + "localized_string_interpolation": { + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "\u26f3 DEFAULT localized_string_interpolation - text: %1$@ - int: %2$lld - float: %3$f" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\ud83c\uddeb\ud83c\uddf7 ADDED localized_string_interpolation - text: %1$@ - int: %2$lld - float: %3$f" + } + } + } + }, + "localized_string_interpolation_with_markdown": { + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "\u26f3 **DEFAULT** *localized_string_interpolation_with_markdown* - **text: %1$@** - *int: %2$lld* - ***float: %3$f***" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\ud83c\uddeb\ud83c\uddf7 **DEFAULT** *localized_string_interpolation_with_markdown* - **text: %1$@** - *int: %2$lld* - ***float: %3$f***" + } + } + } + }, + "localized_string_with_markdown": { + "extractionState": "extracted_with_value", + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "\u26f3 **DEFAULT** *localized_string_with_markdown*" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\ud83c\uddeb\ud83c\uddf7 **DEFAULT** *localized_string_with_markdown*" + } + } + } + } + } +} diff --git a/Modules/LocalizationKit/Examples/LocalizationKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Modules/LocalizationKit/Examples/LocalizationKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/LocalizationKit/Examples/LocalizationKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/LocalizationKit/Examples/LocalizationKitExample/Sources/LocalizationView.swift b/Modules/LocalizationKit/Examples/LocalizationKitExample/Sources/LocalizationView.swift new file mode 100644 index 0000000000..8930c13391 --- /dev/null +++ b/Modules/LocalizationKit/Examples/LocalizationKitExample/Sources/LocalizationView.swift @@ -0,0 +1,122 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit +import SwiftUI + +extension Locale { + static let enUS = Locale(identifier: "en_US") + static let enCA = Locale(identifier: "en_CA") + static let frFR = Locale(identifier: "fr_FR") + static let frCA = Locale(identifier: "fr_CA") +} + +extension l10n { + static let localizedStringNoDefault = LocalizedString("localized_string_NO_default", value: "", comment: "") + + static let localizedStringWithDefault = LocalizedString( + "localized_string_WITH_default", value: "⛳ DEFAULT localized_string_WITH_default", comment: "" + ) + + static let localizedStringInterpolation = LocalizedStringInterpolation( + "localized_string_interpolation", + value: "⛳ DEFAULT localized_string_interpolation - text: %@ - int: %lld - float: %f", comment: "" + ) + + static let localizedStringWithMarkdown = LocalizedString( + "localized_string_with_markdown", value: "⛳ **DEFAULT** *localized_string_with_markdown*", comment: "" + ) + + static let localizedStringInterpolationWithMarkdown = LocalizedStringInterpolation( + "localized_string_interpolation_with_markdown", + value: + "⛳ **DEFAULT** *localized_string_interpolation_with_markdown* - **text: %@** - *int: %lld* - ***float: %f***", + comment: "" + ) +} + +// MARK: - LocalizationView + +struct LocalizationView: View { +// @Environment(\.locale) var locale + let language = l10n.language + + let nameValue = "John *Doe* [link]" + let intValue = 42 + let floatValue = 3.14 + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text(verbatim: "Current language: \(l10n.language)") + .font(.largeTitle) + Text(verbatim: "Current locale: \(Locale.current)") + .font(.largeTitle) + + VStack(alignment: .leading) { + Text(verbatim: "localized_string_NO_default") + .bold() + Text(l10n.localizedStringNoDefault) + } + + VStack(alignment: .leading) { + Text(verbatim: "localized_string_WITH_default") + .bold() + Text(l10n.localizedStringWithDefault) + } + + VStack(alignment: .leading) { + Text(verbatim: "localized_string_interpolation") + .bold() + Text(l10n.localizedStringInterpolation(self.nameValue, self.intValue, self.floatValue)) + } + + VStack(alignment: .leading) { + Text(verbatim: "localized_string_with_markdown") + .bold() + Text(l10n.localizedStringWithMarkdown) + } + + VStack(alignment: .leading) { + Text(verbatim: "localized_string_interpolation_with_markdown") + .bold() + Text(l10n.localizedStringInterpolationWithMarkdown(self.nameValue, self.intValue, self.floatValue)) + } + } + .onAppear { + struct Widget { + let locale: Locale + let value: String + var language: Locale.LanguageCode { self.locale.language.languageCode! } + } + + let widgets: [Widget] = [ + Widget(locale: .frFR, value: "Bonjour"), + Widget(locale: .frFR, value: "Monde"), + Widget(locale: .frFR, value: "Ananas"), + Widget(locale: .enUS, value: "Hello"), + Widget(locale: .enUS, value: "World"), + Widget(locale: .enUS, value: "Pineapple"), + ] + + widgets.filter { $0.language == l10n.language }.forEach { + print($0) + } + + print("bundle: \(Bundle.main.preferredLocalizations[0])") + print("preferred: \(l10n.preferred)") + print("locale: \(l10n.locale)") + print("language: \(l10n.language)") + } + } +} + +#Preview { + VStack(spacing: 80) { + LocalizationView() + .environment(\.locale, .init(identifier: "en")) + + LocalizationView() + .environment(\.locale, .init(identifier: "fr")) + } +} diff --git a/Modules/LocalizationKit/Examples/LocalizationKitExample/Sources/MainApp.swift b/Modules/LocalizationKit/Examples/LocalizationKitExample/Sources/MainApp.swift new file mode 100644 index 0000000000..dbc6ecec56 --- /dev/null +++ b/Modules/LocalizationKit/Examples/LocalizationKitExample/Sources/MainApp.swift @@ -0,0 +1,15 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit +import SwiftUI + +@main +struct LocalizationKitExample: App { + var body: some Scene { + WindowGroup { + MainView() + } + } +} diff --git a/Modules/LocalizationKit/Examples/LocalizationKitExample/Sources/MainView.swift b/Modules/LocalizationKit/Examples/LocalizationKitExample/Sources/MainView.swift new file mode 100644 index 0000000000..2609b279a9 --- /dev/null +++ b/Modules/LocalizationKit/Examples/LocalizationKitExample/Sources/MainView.swift @@ -0,0 +1,16 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit +import SwiftUI + +struct MainView: View { + var body: some View { + LocalizationView() + } +} + +#Preview { + MainView() +} diff --git a/Modules/LocalizationKit/Project.swift b/Modules/LocalizationKit/Project.swift new file mode 100644 index 0000000000..46c07aa6fa --- /dev/null +++ b/Modules/LocalizationKit/Project.swift @@ -0,0 +1,20 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.module( + name: "LocalizationKit", + examples: [ + ModuleExample( + name: "LocalizationKitExample" + ), + ], + dependencies: [ + // no deps + ] +) diff --git a/Modules/LocalizationKit/Resources/Assets.xcassets/Contents.json b/Modules/LocalizationKit/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/LocalizationKit/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/LocalizationKit/Sources/l10n+Locales.swift b/Modules/LocalizationKit/Sources/l10n+Locales.swift new file mode 100644 index 0000000000..56154c71d9 --- /dev/null +++ b/Modules/LocalizationKit/Sources/l10n+Locales.swift @@ -0,0 +1,19 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import SwiftUI + +public extension l10n { + static var locale: Locale = .current + + static var language: Locale.LanguageCode = locale.language.languageCode ?? .english + + static var preferred: Locale.LanguageCode = .init(Bundle.main.preferredLocalizations[0]) + + static var availableLocales: [Locale] = [ + .init(identifier: "en_US"), + .init(identifier: "fr_FR"), + ] +} diff --git a/Modules/LocalizationKit/Sources/l10n+LocalizedString.swift b/Modules/LocalizationKit/Sources/l10n+LocalizedString.swift new file mode 100644 index 0000000000..32c9c6e4c4 --- /dev/null +++ b/Modules/LocalizationKit/Sources/l10n+LocalizedString.swift @@ -0,0 +1,71 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +// swift-format-ignore: AlwaysUseLowerCamelCase +// swiftlint:disable identifier_name + +// ? LOCALIZED_STRING_MACRO_NAMES +// ? See https://forums.developer.apple.com/forums/thread/736941 +// ? Format +// ? func NSLocalizedString( +// ? _ key: String, +// ? tableName: String? = nil, +// ? bundle: Bundle = Bundle.main, +// ? value: String = "", +// ? comment: String +// ? ) -> String + +public extension l10n { + static func LocalizedString( + _ key: StaticString, bundle: Bundle? = nil, value: String.LocalizationValue, comment: StaticString, dsoHandle: UnsafeRawPointer = #dsohandle + ) + -> AttributedString + { + var dlInformation = dl_info() + _ = dladdr(dsoHandle, &dlInformation) + + let path = String(cString: dlInformation.dli_fname) + let url = URL(fileURLWithPath: path).deletingLastPathComponent() + let bundle = bundle ?? Bundle(url: url) + + let string = String(localized: key, defaultValue: value, bundle: bundle, comment: comment) + let markdown = + (try? AttributedString( + markdown: string, + options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace) + )) + ?? AttributedString(string) + return markdown + } + + static func LocalizedStringInterpolation( + _ key: StaticString, bundle: Bundle? = nil, value: String.LocalizationValue, comment: StaticString, dsoHandle: UnsafeRawPointer = #dsohandle + ) -> ( + (CVarArg...) -> AttributedString + ) { + func localizedArgsOnly(_ arguments: CVarArg...) -> AttributedString { + var dlInformation = dl_info() + _ = dladdr(dsoHandle, &dlInformation) + + let path = String(cString: dlInformation.dli_fname) + let url = URL(fileURLWithPath: path).deletingLastPathComponent() + let bundle = bundle ?? Bundle(url: url) + + let format = String(localized: key, defaultValue: value, bundle: bundle, comment: comment) + let string = String(format: format, arguments: arguments) + let markdown = + (try? AttributedString( + markdown: string, + options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace) + )) ?? AttributedString(string) + return markdown + } + + return localizedArgsOnly + } +} + +// swiftlint:enable identifier_name diff --git a/Modules/LocalizationKit/Sources/l10n.swift b/Modules/LocalizationKit/Sources/l10n.swift new file mode 100644 index 0000000000..eceeeaa91d --- /dev/null +++ b/Modules/LocalizationKit/Sources/l10n.swift @@ -0,0 +1,7 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +public enum l10n {} diff --git a/Modules/LocalizationKit/Tests/LocalizationKit_Tests.swift b/Modules/LocalizationKit/Tests/LocalizationKit_Tests.swift new file mode 100644 index 0000000000..0c08fdfa66 --- /dev/null +++ b/Modules/LocalizationKit/Tests/LocalizationKit_Tests.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +final class LocalizationKit_Tests: XCTestCase { + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/Modules/LogKit/Examples/LogKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Modules/LogKit/Examples/LogKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Modules/LogKit/Examples/LogKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/LogKit/Examples/LogKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Modules/LogKit/Examples/LogKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Modules/LogKit/Examples/LogKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/LogKit/Examples/LogKitExample/Resources/Assets.xcassets/Contents.json b/Modules/LogKit/Examples/LogKitExample/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/LogKit/Examples/LogKitExample/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/LogKit/Examples/LogKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Modules/LogKit/Examples/LogKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/LogKit/Examples/LogKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/LogKit/Examples/LogKitExample/Sources/MainApp.swift b/Modules/LogKit/Examples/LogKitExample/Sources/MainApp.swift new file mode 100644 index 0000000000..bd7f364224 --- /dev/null +++ b/Modules/LogKit/Examples/LogKitExample/Sources/MainApp.swift @@ -0,0 +1,24 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LogKit +import SwiftUI + +let log = LogKit.createLoggerFor(app: "LogKitExample") + +// MARK: - LogKitExample + +@main +struct LogKitExample: App { + let module = NewModule() + + var body: some Scene { + WindowGroup { + MainView() + .onAppear { + self.module.doSomething() + } + } + } +} diff --git a/Modules/LogKit/Examples/LogKitExample/Sources/MainView.swift b/Modules/LogKit/Examples/LogKitExample/Sources/MainView.swift new file mode 100644 index 0000000000..bd65544804 --- /dev/null +++ b/Modules/LogKit/Examples/LogKitExample/Sources/MainView.swift @@ -0,0 +1,29 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct MainView: View { + @State var count = 1 + + var body: some View { + Text("Hello, LogKit!") + .onAppear { + log.trace("This is a trace") + log.debug("Hello, World from LogKitExample!") + log.info("Some info perhaps?") + log.warning("gosh! be careful") + log.error("ooops... something went wrong") + log.critical("WE MUST ABORT") + } + .onTapGesture { + log.debug("touched \(self.count)") + self.count += 1 + } + } +} + +#Preview { + MainView() +} diff --git a/Modules/LogKit/Examples/LogKitExample/Sources/NewModule.swift b/Modules/LogKit/Examples/LogKitExample/Sources/NewModule.swift new file mode 100644 index 0000000000..0efb4c3f81 --- /dev/null +++ b/Modules/LogKit/Examples/LogKitExample/Sources/NewModule.swift @@ -0,0 +1,24 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import LogKit + +public struct NewModule { + // MARK: Lifecycle + + public init() { + self.log.info("new module has been initialized") + } + + // MARK: Public + + public func doSomething() { + self.log.info("doing something") + } + + // MARK: Private + + private let log = LogKit.createLoggerFor(module: "NewModule") +} diff --git a/Modules/LogKit/Project.swift b/Modules/LogKit/Project.swift new file mode 100644 index 0000000000..e544a05e25 --- /dev/null +++ b/Modules/LogKit/Project.swift @@ -0,0 +1,20 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.module( + name: "LogKit", + examples: [ + ModuleExample( + name: "LogKitExample" + ), + ], + dependencies: [ + .external(name: "Logging"), + ] +) diff --git a/Modules/LogKit/Resources/Assets.xcassets/Contents.json b/Modules/LogKit/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/LogKit/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/LogKit/Sources/LogKit.swift b/Modules/LogKit/Sources/LogKit.swift new file mode 100644 index 0000000000..95eaca2544 --- /dev/null +++ b/Modules/LogKit/Sources/LogKit.swift @@ -0,0 +1,37 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Logging + +public struct LogKit { + // MARK: Lifecycle + + private init() { + // nothing to do + } + + // MARK: Public + + public static func createLoggerFor(module: String) -> Logger { + self.createLogger(label: "mod:\(module)") + } + + public static func createLoggerFor(app: String) -> Logger { + self.createLogger(label: "app:\(app)") + } + + // MARK: Private + + private static var hasBeenInitialized: Bool = false + + private static func createLogger(label: String) -> Logger { + if !self.hasBeenInitialized { + LoggingSystem.bootstrap(LogKitLogHandler.standardOutput) + self.hasBeenInitialized = true + } + + let logger = Logger(label: "\(label)") + return logger + } +} diff --git a/Modules/LogKit/Sources/LogKitLogHandler.swift b/Modules/LogKit/Sources/LogKitLogHandler.swift new file mode 100644 index 0000000000..ee598d048f --- /dev/null +++ b/Modules/LogKit/Sources/LogKitLogHandler.swift @@ -0,0 +1,184 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Darwin +import Foundation +import Logging + +// ? Note: taken from https://github.com/apple/swift-log/blob/main/Sources/Logging/Logging.swift + +// swiftlint:disable function_parameter_count + +extension String { + var fileURL: URL { + URL(fileURLWithPath: self) + } + + var pathExtension: String { + self.fileURL.pathExtension + } + + var lastPathComponent: String { + self.fileURL.lastPathComponent + } +} + +let systemStderr = Darwin.stderr +let systemStdout = Darwin.stdout + +typealias CFilePointer = UnsafeMutablePointer + +// MARK: - StdioOutputStream + +struct StdioOutputStream: TextOutputStream { + enum FlushMode { + case undefined + case always + } + + static let stderr = StdioOutputStream(file: systemStderr, flushMode: .always) + static let stdout = StdioOutputStream(file: systemStdout, flushMode: .always) + + let file: CFilePointer + let flushMode: FlushMode + + func write(_ string: String) { + self.contiguousUTF8(string) + .withContiguousStorageIfAvailable { utf8Bytes in + flockfile(self.file) + defer { + funlockfile(self.file) + } + _ = fwrite(utf8Bytes.baseAddress!, 1, utf8Bytes.count, self.file) + if case .always = self.flushMode { + self.flush() + } + }! + } + + func flush() { + _ = fflush(self.file) + } + + func contiguousUTF8(_ string: String) -> String.UTF8View { + var contiguousString = string + contiguousString.makeContiguousUTF8() + return contiguousString.utf8 + } +} + +// MARK: - LogKitLogHandler + +public struct LogKitLogHandler: LogHandler { + // MARK: Lifecycle + + // internal for testing only + init(label: String, stream: _SendableTextOutputStream) { + self.init(label: label, stream: stream, metadataProvider: LoggingSystem.metadataProvider) + } + + // internal for testing only + init(label: String, stream: _SendableTextOutputStream, metadataProvider: Logger.MetadataProvider?) { + self.label = label + self.stream = stream + self.metadataProvider = metadataProvider + } + + // MARK: Public + + public var logLevel: Logger.Level = .trace + + public var metadataProvider: Logger.MetadataProvider? + + public var metadata = Logger.Metadata() + + /// Factory that makes a `LogKitLogHandler` to directs its output to `stdout` + public static func standardOutput(label: String) -> LogKitLogHandler { + LogKitLogHandler( + label: label, stream: StdioOutputStream.stdout, metadataProvider: LoggingSystem.metadataProvider + ) + } + + /// Factory that makes a `LogKitLogHandler` that directs its output to `stdout` + public static func standardOutput(label: String, metadataProvider: Logger.MetadataProvider?) -> LogKitLogHandler { + LogKitLogHandler(label: label, stream: StdioOutputStream.stdout, metadataProvider: metadataProvider) + } + + public subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { + get { + self.metadata[metadataKey] + } + set { + self.metadata[metadataKey] = newValue + } + } + + public func log( + level: Logger.Level, + message: Logger.Message, + metadata _: Logger.Metadata?, + source _: String, + file: String, + function: String, + line: UInt + ) { + var strm = self.stream + + strm.write( + "\(self.timestamp()) \(self.prettyLevel(level)) [\(self.label)](\(self.prettyFile(file)):\(line)) \(function) > \(message)\n" + ) + } + + // MARK: Internal + + typealias _SendableTextOutputStream = TextOutputStream & Sendable + + // MARK: Private + + private let stream: _SendableTextOutputStream + private let label: String + + private var prettyMetadata: String? + + private func prettyLevel(_ level: Logger.Level) -> String { + switch level { + case .trace: + "🩶 [TRCE]" + case .debug: + "💚 [DBUG]" + case .info, + .notice: + "💙 [INFO]" + case .warning: + "⚠️ [WARN]" + case .error: + "❌ [ERR ]" + case .critical: + "💥 [CRIT]" + } + } + + private func prettyFile(_ file: String) -> String { + file.lastPathComponent + } + + private func timestamp() -> String { + var buffer = [Int8](repeating: 0, count: 255) + var timestamp = time(nil) + let localTime = localtime(×tamp) + let milliseconds = Int(Date().timeIntervalSince1970 * 1000) % 1000 + + strftime(&buffer, buffer.count, "%H:%M:%S%", localTime) + + buffer.replaceSubrange(8..<12, with: String(format: ".%03d", milliseconds).utf8CString) + + return buffer.withUnsafeBufferPointer { + $0.withMemoryRebound(to: CChar.self) { + String(cString: $0.baseAddress!) + } + } + } +} + +// swiftlint:enable function_parameter_count diff --git a/Modules/LogKit/Tests/LogKit_Tests.swift b/Modules/LogKit/Tests/LogKit_Tests.swift new file mode 100644 index 0000000000..24ee709a9a --- /dev/null +++ b/Modules/LogKit/Tests/LogKit_Tests.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +final class LogKit_Tests: XCTestCase { + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/Modules/RobotKit/Examples/RobotKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Modules/RobotKit/Examples/RobotKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Modules/RobotKit/Examples/RobotKitExample/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/RobotKit/Examples/RobotKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Modules/RobotKit/Examples/RobotKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Modules/RobotKit/Examples/RobotKitExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/RobotKit/Examples/RobotKitExample/Resources/Assets.xcassets/Contents.json b/Modules/RobotKit/Examples/RobotKitExample/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/RobotKit/Examples/RobotKitExample/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/RobotKit/Examples/RobotKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/Modules/RobotKit/Examples/RobotKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/RobotKit/Examples/RobotKitExample/Resources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/RobotKit/Examples/RobotKitExample/Sources/MainApp.swift b/Modules/RobotKit/Examples/RobotKitExample/Sources/MainApp.swift new file mode 100644 index 0000000000..f7e7772e2f --- /dev/null +++ b/Modules/RobotKit/Examples/RobotKitExample/Sources/MainApp.swift @@ -0,0 +1,14 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +@main +struct RobotKitExample: App { + var body: some Scene { + WindowGroup { + MainView() + } + } +} diff --git a/Modules/RobotKit/Examples/RobotKitExample/Sources/MainView.swift b/Modules/RobotKit/Examples/RobotKitExample/Sources/MainView.swift new file mode 100644 index 0000000000..00549719e3 --- /dev/null +++ b/Modules/RobotKit/Examples/RobotKitExample/Sources/MainView.swift @@ -0,0 +1,45 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import RobotKit +import SwiftUI + +struct MainView: View { + // MARK: Internal + + var body: some View { + NavigationStack { + VStack(spacing: 30) { + ConnectedRobotInformationView() + + Button { + self.presentRobotConnection.toggle() + } label: { + Text("Connect robot") + } + .fullScreenCover(isPresented: self.$presentRobotConnection) { + RobotConnectionView(viewModel: RobotConnectionViewModel()) + } + + NavigationLink( + destination: { + RobotControlView(viewModel: RobotControlViewModel(robot: Robot.shared)) + }, + label: { + Text("Go to robot control") + } + ) + } + .navigationTitle("RobotKit Explorer") + } + } + + // MARK: Private + + @State private var presentRobotConnection: Bool = false +} + +#Preview { + MainView() +} diff --git a/Modules/RobotKit/Examples/RobotKitExample/Sources/ViewModels/RobotControlViewModel.swift b/Modules/RobotKit/Examples/RobotKitExample/Sources/ViewModels/RobotControlViewModel.swift new file mode 100644 index 0000000000..cdea41e05e --- /dev/null +++ b/Modules/RobotKit/Examples/RobotKitExample/Sources/ViewModels/RobotControlViewModel.swift @@ -0,0 +1,43 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import RobotKit +import SwiftUI + +class RobotControlViewModel: ObservableObject { + // MARK: Lifecycle + + init(robot: Robot) { + self.robot = robot + self.robot.onMagicCard() + .receive(on: DispatchQueue.main) + .sink { + self.magicCard = $0 + self.magicCardImage = { + switch $0 { + case .none: + Image(systemName: "cross") + case .emergency_stop: + Image(systemName: "exclamationmark.octagon") + case .dice_roll: + Image(systemName: "dice") + default: + Image(systemName: "photo") + } + }($0) + } + .store(in: &self.cancellables) + } + + // MARK: Internal + + @Published var magicCard: MagicCard = .none + @Published var magicCardImage: Image = .init(systemName: "photo") + + // MARK: Private + + private let robot: Robot + private var cancellables: Set = [] +} diff --git a/Modules/RobotKit/Examples/RobotKitExample/Sources/Views/ConnectedRobotInformationView.swift b/Modules/RobotKit/Examples/RobotKitExample/Sources/Views/ConnectedRobotInformationView.swift new file mode 100644 index 0000000000..63a4dec978 --- /dev/null +++ b/Modules/RobotKit/Examples/RobotKitExample/Sources/Views/ConnectedRobotInformationView.swift @@ -0,0 +1,46 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import RobotKit +import SwiftUI + +struct ConnectedRobotInformationView: View { + @StateObject var viewModel: ConnectedRobotInformationViewModel = .init() + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text("Connected Robot Information") + .font(.title2) + .underline() + + Grid(alignment: .leading, horizontalSpacing: 100) { + GridRow { + Text("Name:") + Text(self.viewModel.name) + } + GridRow { + Text("S/N:") + Text(self.viewModel.serialNumber) + } + GridRow { + Text("LekaOS:") + Text(self.viewModel.osVersion) + } + GridRow { + Text("Battery:") + Text("\(self.viewModel.battery)%") + } + GridRow { + Text("Charging:") + Text(self.viewModel.isCharging ? "yes" : "no") + } + } + } + } +} + +#Preview { + ConnectedRobotInformationView() +} diff --git a/Modules/RobotKit/Examples/RobotKitExample/Sources/Views/RobotControlView.swift b/Modules/RobotKit/Examples/RobotKitExample/Sources/Views/RobotControlView.swift new file mode 100644 index 0000000000..93a3383b55 --- /dev/null +++ b/Modules/RobotKit/Examples/RobotKitExample/Sources/Views/RobotControlView.swift @@ -0,0 +1,138 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import RobotKit +import SwiftUI + +struct RobotControlView: View { + // MARK: Lifecycle + + init(viewModel: RobotControlViewModel) { + _viewModel = StateObject(wrappedValue: viewModel) + } + + // MARK: Internal + + @StateObject var viewModel: RobotControlViewModel + + var body: some View { + VStack { + VStack(alignment: .leading, spacing: 30) { + VStack(alignment: .leading, spacing: 5) { + Text("Motion") + .font(.title) + HStack { + RobotControlActionButton(title: "Move forward", image: "arrow.up", tint: .orange) { + self.robot.move(.forward(speed: 1.0)) + } + RobotControlActionButton(title: "Move backward", image: "arrow.down", tint: .green) { + self.robot.move(.backward(speed: 0.5)) + } + RobotControlActionButton(title: "Spin clockwise", image: "arrow.clockwise", tint: .indigo) { + self.robot.move(.spin(.clockwise, speed: 0.7)) + } + RobotControlActionButton( + title: "Spin counterclockwise", image: "arrow.counterclockwise", tint: .teal + ) { + self.robot.move(.spin(.counterclockwise, speed: 0.7)) + } + RobotControlActionButton(title: "Stop motion", image: "xmark", tint: .red) { + self.robot.stopMotion() + } + } + } + + VStack(alignment: .leading, spacing: 5) { + Text("Lights") + .font(.title) + HStack { + RobotControlActionButton(title: "Individual LEDs", image: "light.max", tint: .orange) { + self.robot.shine(.spot(.belt, ids: [0, 4, 8, 10, 12], in: .red)) + } + RobotControlActionButton(title: "Quarters", image: "light.max", tint: .green) { + self.robot.shine(.quarterFrontLeft(in: .blue)) + self.robot.shine(.quarterFrontRight(in: .red)) + self.robot.shine(.quarterBackLeft(in: .red)) + self.robot.shine(.quarterBackRight(in: .blue)) + } + RobotControlActionButton(title: "Halves", image: "light.max", tint: .indigo) { + self.robot.shine(.halfRight(in: .green)) + self.robot.shine(.halfLeft(in: .red)) + } + RobotControlActionButton(title: "Full belt + ears", image: "light.max", tint: .teal) { + self.robot.shine(.full(.belt, in: .blue)) + self.robot.shine(.full(.ears, in: .green)) + } + RobotControlActionButton(title: "Turn off lights", image: "xmark", tint: .red) { + self.robot.stopLights() + } + } + } + + VStack(alignment: .leading, spacing: 5) { + Text("Reinforcers") + .font(.title) + HStack { + RobotControlActionButton(title: "Rainbow", image: "number.circle", tint: .orange) { + self.robot.run(.rainbow) + } + RobotControlActionButton(title: "Fire", image: "number.circle", tint: .green) { + self.robot.run(.fire) + } + RobotControlActionButton(title: "Spin 1", image: "number.circle", tint: .indigo) { + self.robot.run(.spinBlinkGreenOff) + } + RobotControlActionButton(title: "Spin 2", image: "number.circle", tint: .teal) { + self.robot.run(.spinBlinkBlueViolet) + } + } + } + + VStack(alignment: .leading, spacing: 5) { + Text("Magic Cards") + .font(.title) + HStack(alignment: .center, spacing: 30) { + Text("ID: 0x\(String(format: "%04X", self.viewModel.magicCard.id))") + .monospacedDigit() + self.viewModel.magicCardImage + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 180) + .foregroundColor(.blue) + } + } + } + } + .navigationTitle("Robot Control") + .toolbar { + ToolbarItemGroup(placement: .topBarTrailing) { + self.stopButton + } + } + } + + var stopButton: some View { + Button { + self.robot.stop() + } label: { + Image(systemName: "exclamationmark.octagon.fill") + Text("STOP") + .bold() + } + .buttonStyle(.robotControlPlainButtonStyle(foreground: .white, background: .red)) + } + + // MARK: Private + + private let robot = Robot.shared +} + +#Preview { + let viewModel = RobotControlViewModel(robot: Robot.shared) + + return NavigationStack { + RobotControlView(viewModel: viewModel) + } +} diff --git a/Modules/RobotKit/Project.swift b/Modules/RobotKit/Project.swift new file mode 100644 index 0000000000..a6e93ed736 --- /dev/null +++ b/Modules/RobotKit/Project.swift @@ -0,0 +1,24 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription +import ProjectDescriptionHelpers + +let project = Project.module( + name: "RobotKit", + examples: [ + ModuleExample( + name: "RobotKitExample" + ), + ], + dependencies: [ + .project(target: "BLEKit", path: Path("../../Modules/BLEKit")), + .project(target: "DesignKit", path: Path("../../Modules/DesignKit")), + .project(target: "LogKit", path: Path("../../Modules/LogKit")), + .project(target: "LocalizationKit", path: Path("../../Modules/LocalizationKit")), + .external(name: "Version"), + ] +) diff --git a/Modules/RobotKit/Resources/Assets.xcassets/Contents.json b/Modules/RobotKit/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Modules/RobotKit/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/RobotKit/Resources/Localizable.xcstrings b/Modules/RobotKit/Resources/Localizable.xcstrings new file mode 100644 index 0000000000..41947d4314 --- /dev/null +++ b/Modules/RobotKit/Resources/Localizable.xcstrings @@ -0,0 +1,114 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "robotkit.robot_connect_view.cancel_button" : { + "comment" : "The title of the cancel button in the toolbar", + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Cancel" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annuler" + } + } + } + }, + "robotkit.robot_connect_view.connect_button" : { + "comment" : "The title of the connect button", + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Connect" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se connecter" + } + } + } + }, + "robotkit.robot_connect_view.continue_button" : { + "comment" : "The title of the continue button", + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Continue" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Continuer" + } + } + } + }, + "robotkit.robot_connect_view.disconnect_button" : { + "comment" : "The title of the disconnect button", + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Disconnect" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Se déconnecter" + } + } + } + }, + "robotkit.robot_connect_view.navigation_title" : { + "comment" : "The title of the robot connection view", + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Connect to a robot" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connecter un robot" + } + } + } + }, + "robotkit.robot_connect_view.searching_view_text" : { + "comment" : "The text displayed in the searching view", + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Searching for robots..." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recherche en cours..." + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Modules/RobotKit/Sources/Extensions/Array+checksum8.swift b/Modules/RobotKit/Sources/Extensions/Array+checksum8.swift new file mode 100644 index 0000000000..5bf6f1e416 --- /dev/null +++ b/Modules/RobotKit/Sources/Extensions/Array+checksum8.swift @@ -0,0 +1,15 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +public extension [UInt8] { + var checksum8: UInt8 { + var checksum = 0 + + for value in self { + checksum = (Int(value) + checksum) % 256 + } + + return UInt8(checksum) + } +} diff --git a/Modules/RobotKit/Sources/MagicCards.swift b/Modules/RobotKit/Sources/MagicCards.swift new file mode 100644 index 0000000000..91d80e5558 --- /dev/null +++ b/Modules/RobotKit/Sources/MagicCards.swift @@ -0,0 +1,112 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +// MARK: - MagicCard + +// swiftlint:disable identifier_name + +public struct MagicCard: Equatable { + // MARK: Lifecycle + + public init(language: Language = .none, id: UInt16) { + self.language = language + self.id = id + } + + // MARK: Public + + public enum Language: UInt8 { + case none = 0 + case fr_FR = 1 + case en_US = 2 + } + + public let language: Language + public let id: UInt16 + + public static func == (lhs: MagicCard, rhs: MagicCard) -> Bool { + lhs.id == rhs.id + } + + public static func === (lhs: MagicCard, rhs: MagicCard) -> Bool { + lhs.id == rhs.id && lhs.language == rhs.language + } +} + +public extension MagicCard { + static let none: MagicCard = .init(id: 0x0000) + static let emergency_stop: MagicCard = .init(id: 0x0001) + static let dice_roll: MagicCard = .init(id: 0x0002) + + static let color_purple: MagicCard = .init(id: 0x0003) + static let color_indigo: MagicCard = .init(id: 0x0004) + static let color_blue: MagicCard = .init(id: 0x0005) + static let color_green: MagicCard = .init(id: 0x0006) + static let color_yellow: MagicCard = .init(id: 0x0007) + static let color_orange: MagicCard = .init(id: 0x0008) + static let color_red: MagicCard = .init(id: 0x0009) + + static let number_0: MagicCard = .init(id: 0x000A) + static let number_1: MagicCard = .init(id: 0x000B) + static let number_2: MagicCard = .init(id: 0x000C) + static let number_3: MagicCard = .init(id: 0x000D) + static let number_4: MagicCard = .init(id: 0x000E) + static let number_5: MagicCard = .init(id: 0x000F) + static let number_6: MagicCard = .init(id: 0x0010) + static let number_7: MagicCard = .init(id: 0x0011) + static let number_8: MagicCard = .init(id: 0x0012) + static let number_9: MagicCard = .init(id: 0x0013) + static let number_10: MagicCard = .init(id: 0x0014) + + static let shape_square: MagicCard = .init(id: 0x0015) + static let shape_circle: MagicCard = .init(id: 0x0016) + static let shape_triangle: MagicCard = .init(id: 0x0017) + static let shape_star: MagicCard = .init(id: 0x0018) + + static let activity_music_quest: MagicCard = .init(id: 0x0019) + static let activity_super_simon: MagicCard = .init(id: 0x001A) + static let activity_colored_quest: MagicCard = .init(id: 0x001B) + static let activity_music_colored_board: MagicCard = .init(id: 0x001C) + static let activity_hide_and_seek: MagicCard = .init(id: 0x001D) + static let activity_colors_and_sounds: MagicCard = .init(id: 0x001E) + static let activity_magic_objects: MagicCard = .init(id: 0x001F) + static let activity_dance_freeze: MagicCard = .init(id: 0x0020) + + static let remote_standard: MagicCard = .init(id: 0x0021) + static let remote_colored_arrows: MagicCard = .init(id: 0x0022) + + static let reinforcer_1_blink_green: MagicCard = .init(id: 0x0023) + static let reinforcer_2_spin_blink: MagicCard = .init(id: 0x0024) + static let reinforcer_3_fire: MagicCard = .init(id: 0x0025) + static let reinforcer_4_sprinkles: MagicCard = .init(id: 0x0026) + static let reinforcer_5_rainbow: MagicCard = .init(id: 0x0027) + + static let emotion_fear_child: MagicCard = .init(id: 0x0028) + static let emotion_disgust_child: MagicCard = .init(id: 0x0029) + static let emotion_anger_child: MagicCard = .init(id: 0x002A) + static let emotion_joy_child: MagicCard = .init(id: 0x002B) + static let emotion_sadness_child: MagicCard = .init(id: 0x002C) + static let emotion_fear_leka: MagicCard = .init(id: 0x002D) + static let emotion_disgust_leka: MagicCard = .init(id: 0x002E) + static let emotion_anger_leka: MagicCard = .init(id: 0x002F) + static let emotion_joy_leka: MagicCard = .init(id: 0x0030) + static let emotion_sadness_leka: MagicCard = .init(id: 0x0031) + + static let vegetable_carrot_orange: MagicCard = .init(id: 0x0032) + static let vegetable_potato_yellow: MagicCard = .init(id: 0x0033) + static let vegetable_salad_green: MagicCard = .init(id: 0x0034) + static let vegetable_mushroom_grey: MagicCard = .init(id: 0x0035) + static let fruit_strawberry_red: MagicCard = .init(id: 0x0036) + static let fruit_cherry_pink: MagicCard = .init(id: 0x0037) + static let fruit_apple_green: MagicCard = .init(id: 0x0038) + static let fruit_banana_yellow: MagicCard = .init(id: 0x0039) + static let fruit_grapes_black: MagicCard = .init(id: 0x003A) + + static let math_arithmetic_substraction_sign_minus: MagicCard = .init(id: 0x003B) + static let math_arithmetic_addition_sign_plus: MagicCard = .init(id: 0x003C) +} + +// swiftlint:enable identifier_name diff --git a/Modules/RobotKit/Sources/Robot+BLE.swift b/Modules/RobotKit/Sources/Robot+BLE.swift new file mode 100644 index 0000000000..2e97492a5f --- /dev/null +++ b/Modules/RobotKit/Sources/Robot+BLE.swift @@ -0,0 +1,28 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import Combine +import Foundation + +extension Robot { + func subscribeToBLEConnectionUpdates() { + BLEManager.shared.didConnect + .receive(on: DispatchQueue.main) + .sink { + self.connectedPeripheral = $0 + self.isConnected.send(true) + self.name.send($0.peripheral.name ?? "(n/a)") + } + .store(in: &cancellables) + + BLEManager.shared.didDisconnect + .receive(on: DispatchQueue.main) + .sink { + self.connectedPeripheral = nil + self.isConnected.send(false) + } + .store(in: &cancellables) + } +} diff --git a/Modules/RobotKit/Sources/Robot+Colors.swift b/Modules/RobotKit/Sources/Robot+Colors.swift new file mode 100644 index 0000000000..407f16817b --- /dev/null +++ b/Modules/RobotKit/Sources/Robot+Colors.swift @@ -0,0 +1,123 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// MARK: - Robot.Color + +// swiftlint:disable nesting identifier_name line_length + +public extension Robot { + struct Color { + // MARK: Lifecycle + + public init(robot rRGB: UInt8..., screen sRGB: UInt8...) { + guard rRGB.count == 3, sRGB.count == 3 else { fatalError() } + + self.robotRGB = rRGB + self.screenRGB = sRGB + } + + public init(from value: String) { + guard let color = ColorString(rawValue: value)?.color else { + fatalError("Invalid color string \(value)") + } + self = color + } + + public init(r: UInt8, g: UInt8, b: UInt8) { + self.robotRGB = [r, g, b] + self.screenRGB = [r, g, b] + } + + public init(fromGradient colors: (Color, Color), at position: Float) { + let positionClamped = max(min(position, 1), 0) + let (r1, g1, b1) = (Float(colors.0.robotRGB[0]), Float(colors.0.robotRGB[1]), Float(colors.0.robotRGB[2])) + let (r2, g2, b2) = (Float(colors.1.robotRGB[0]), Float(colors.1.robotRGB[1]), Float(colors.1.robotRGB[2])) + + let r = UInt8(r1 + (r2 - r1) * positionClamped) + let g = UInt8(g1 + (g2 - g1) * positionClamped) + let b = UInt8(b1 + (b2 - b1) * positionClamped) + + self.robotRGB = [r, g, b] + self.screenRGB = [r, g, b] + } + + // MARK: Public + + public var robot: [UInt8] { + self.robotRGB + } + + public var screen: SwiftUI.Color { + SwiftUI.Color( + red: Double(self.screenRGB[0]) / 255.0, + green: Double(self.screenRGB[1]) / 255.0, + blue: Double(self.screenRGB[2]) / 255.0 + ) + } + + // MARK: Private + + private enum ColorString: String { + case black + case white + case red + case green + case blue + case lightBlue + case orange + case purple + case pink + case yellow + + // MARK: Public + + public var color: Robot.Color { + switch self { + case .black: + .black + case .white: + .white + case .red: + .red + case .green: + .green + case .blue: + .blue + case .lightBlue: + .lightBlue + case .orange: + .orange + case .purple: + .purple + case .pink: + .pink + case .yellow: + .yellow + } + } + } + + private let robotRGB: [UInt8] + private let screenRGB: [UInt8] + } +} + +public extension Robot.Color { + static let black: Robot.Color = .init(robot: 0, 0, 0, screen: 0, 0, 0) + static let white: Robot.Color = .init(robot: 255, 255, 255, screen: 255, 255, 255) + + static let red: Robot.Color = .init(robot: 255, 0, 0, screen: 255, 0, 0) + static let green: Robot.Color = .init(robot: 0, 150, 0, screen: 0, 226, 0) + static let blue: Robot.Color = .init(robot: 0, 0, 255, screen: 0, 121, 255) + + static let lightBlue: Robot.Color = .init(robot: 0, 121, 255, screen: 70, 194, 248) + static let orange: Robot.Color = .init(robot: 248, 100, 0, screen: 255, 143, 0) + static let purple: Robot.Color = .init(robot: 20, 0, 80, screen: 173, 73, 247) + static let pink: Robot.Color = .init(robot: 255, 0, 127, screen: 252, 103, 178) + static let yellow: Robot.Color = .init(robot: 255, 255, 0, screen: 251, 232, 0) +} + +// swiftlint:enable nesting identifier_name line_length diff --git a/Modules/RobotKit/Sources/Robot+Commands.swift b/Modules/RobotKit/Sources/Robot+Commands.swift new file mode 100644 index 0000000000..88bc50fb2b --- /dev/null +++ b/Modules/RobotKit/Sources/Robot+Commands.swift @@ -0,0 +1,27 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +extension Robot { + static let kHeaderPattern: [UInt8] = [0x2A, 0x2A, 0x2A, 0x2A] + + static func commandGenerator(commands: [UInt8]...) -> Data { + self.commandGenerator(commands: commands) + } + + static func commandGenerator(commands: [[UInt8]]) -> Data { + let commands = commands.filter { $0 != [] } + var output: [UInt8] = [] + + output.append(contentsOf: Robot.kHeaderPattern) + output.append(UInt8(commands.count)) + + for command in commands { + output.append(contentsOf: command) + } + + return Data(output) + } +} diff --git a/Modules/RobotKit/Sources/Robot+Information.swift b/Modules/RobotKit/Sources/Robot+Information.swift new file mode 100644 index 0000000000..922a986f5e --- /dev/null +++ b/Modules/RobotKit/Sources/Robot+Information.swift @@ -0,0 +1,89 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import Combine +import Version + +extension Robot { + func registerBatteryCharacteristicNotificationCallback() { + let characteristic = CharacteristicModelNotifying( + characteristicUUID: BLESpecs.Battery.Characteristics.level, + serviceUUID: BLESpecs.Battery.service, + onNotification: { data in + if let value = data?.first { + self.battery.send(Int(value)) +// log.trace("🤖 battery: \(self.battery.value)%") + } + } + ) + + connectedPeripheral?.notifyingCharacteristics.insert(characteristic) + } + + func registerChargingStatusNotificationCallback() { + let characteristic = CharacteristicModelNotifying( + characteristicUUID: BLESpecs.Monitoring.Characteristics.chargingStatus, + serviceUUID: BLESpecs.Monitoring.service, + onNotification: { data in + if let value = data?.first { + self.isCharging.send(value == 1) +// log.trace("🤖 isCharging: \(self.isCharging.value)") + } + } + ) + + connectedPeripheral?.notifyingCharacteristics.insert(characteristic) + } + + func registerOSVersionReadCallback() { + let characteristic = CharacteristicModelReadOnly( + characteristicUUID: BLESpecs.DeviceInformation.Characteristics.osVersion, + serviceUUID: BLESpecs.DeviceInformation.service, + onRead: { data in + if let data { + self.osVersion.send( + Version( + String(decoding: data, as: UTF8.self) + .replacingOccurrences(of: "\0", with: ""))) + log.trace("🤖 osVersion: \(self.osVersion.value)") + } + } + ) + + connectedPeripheral?.readOnlyCharacteristics.insert(characteristic) + } + + func registerSerialNumberReadCallback() { + let characteristic = CharacteristicModelReadOnly( + characteristicUUID: BLESpecs.DeviceInformation.Characteristics.serialNumber, + serviceUUID: BLESpecs.DeviceInformation.service, + onRead: { data in + if let data { + self.serialNumber.send( + String(decoding: data, as: UTF8.self) + .replacingOccurrences(of: "\0", with: "")) + log.trace("🤖 serialNumber: \(self.serialNumber.value)") + } + } + ) + + connectedPeripheral?.readOnlyCharacteristics.insert(characteristic) + } + + func registerChargingStatusReadCallback() { + let characteristic = CharacteristicModelReadOnly( + characteristicUUID: BLESpecs.Monitoring.Characteristics.chargingStatus, + serviceUUID: BLESpecs.Monitoring.service, + onRead: { data in + if let value = data?.first { + self.isCharging.send(value == 1) +// log.trace("🤖 isCharging: \(self.isCharging.value)") + } + } + ) + + connectedPeripheral?.readOnlyCharacteristics.insert(characteristic) + } +} diff --git a/Modules/RobotKit/Sources/Robot+Lights.swift b/Modules/RobotKit/Sources/Robot+Lights.swift new file mode 100644 index 0000000000..623137d34a --- /dev/null +++ b/Modules/RobotKit/Sources/Robot+Lights.swift @@ -0,0 +1,309 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftlint:disable identifier_name nesting cyclomatic_complexity + +public extension Robot { + enum Lights { + case all(in: Color) + + case full(_ position: Full.Position, in: Color) + + case halfLeft(in: Color) + case halfRight(in: Color) + case quarterFrontLeft(in: Color) + case quarterFrontRight(in: Color) + case quarterBackLeft(in: Color) + case quarterBackRight(in: Color) + + case earLeft(in: Color) + case earRight(in: Color) + + case spot(Spot.Position, ids: [UInt8], in: Color) + case range(start: UInt8, end: UInt8, in: Color) + + // MARK: Public + + public enum Spot { + // MARK: Public + + public enum Position: UInt8 { + case ears = 0x11 + case belt = 0x12 + } + + // MARK: Internal + + static let id: UInt8 = 0x10 + } + + public enum Full { + // MARK: Public + + public enum Position: UInt8 { + case ears = 0x14 + case belt = 0x15 + } + + // MARK: Internal + + static let id: UInt8 = 0x13 + } + + public enum Range { + // MARK: Public + + public enum Position: UInt8 { + case ears = 0x17 + case belt = 0x18 + } + + // MARK: Internal + + static let id: UInt8 = 0x16 + } + + public enum Blacken { + case all + case full(_ position: Full.Position) + case halfLeft + case halfRight + case quarterFrontLeft + case quarterFrontRight + case quarterBackLeft + case quarterBackRight + + case earLeft + case earRight + + case spot(Spot.Position, ids: [UInt8]) + case range(start: UInt8, end: UInt8) + } + + public var color: Robot.Color { + switch self { + case let .all(color), + let .full(_, color), + let .halfLeft(color), + let .halfRight(color), + let .quarterFrontLeft(color), + let .quarterFrontRight(color), + let .quarterBackLeft(color), + let .quarterBackRight(color), + let .earLeft(color), + let .earRight(color), + let .spot(_, _, color), + let .range(_, _, color): + color + } + } + + // MARK: Internal + + var cmd: [[UInt8]] { + var output: [[UInt8]] = [[]] + + switch self { + case let .all(color): + let ears = self.shineFull(.ears, in: color) + let belt = self.shineFull(.belt, in: color) + output.append(contentsOf: [ears, belt]) + + case let .spot(_: position, ids, color): + for id in ids { + let payload = self.shineSpot(id, on: position, in: color) + output.append(payload) + } + + case let .full(position, color): + let payload = self.shineFull(position, in: color) + output.append(payload) + + case let .range(start, end, color): + let payload = self.shineRange(from: start, to: end, in: color) + output.append(payload) + + case let .halfLeft(color): + let start: UInt8 = 0 + let end: UInt8 = 9 + let payload = self.shineRange(from: start, to: end, in: color) + output.append(payload) + + case let .halfRight(color): + let start: UInt8 = 10 + let end: UInt8 = 19 + let payload = self.shineRange(from: start, to: end, in: color) + output.append(payload) + + case let .quarterFrontLeft(color): + let start: UInt8 = 0 + let end: UInt8 = 4 + let payload = self.shineRange(from: start, to: end, in: color) + output.append(payload) + + case let .quarterFrontRight(color): + let start: UInt8 = 15 + let end: UInt8 = 19 + let payload = self.shineRange(from: start, to: end, in: color) + output.append(payload) + + case let .quarterBackLeft(color): + let start: UInt8 = 5 + let end: UInt8 = 9 + let payload = self.shineRange(from: start, to: end, in: color) + output.append(payload) + + case let .quarterBackRight(color): + let start: UInt8 = 10 + let end: UInt8 = 14 + let payload = self.shineRange(from: start, to: end, in: color) + output.append(payload) + + case let .earLeft(color): + let payload = self.shineSpot(0, on: .ears, in: color) + output.append(payload) + + case let .earRight(color): + let payload = self.shineSpot(1, on: .ears, in: color) + output.append(payload) + } + + return output + } + + static func spot(on position: Spot.Position, ids: UInt8..., in color: Color) -> Self { + .spot(position, ids: ids, in: color) + } + + static func range(startID: UInt8, endID: UInt8, in color: Color) -> Self { + .range(start: startID, end: endID, in: color) + } + + // MARK: Private + + private func shineSpot(_ id: UInt8, on position: Spot.Position, in color: Color) -> [UInt8] { + var payload: [UInt8] = [] + + payload.append(contentsOf: [ + position.rawValue, + id, + ]) + + payload.append(contentsOf: color.robot) + payload.append(payload.checksum8) + + payload.insert(Spot.id, at: 0) + + return payload + } + + private func shineFull(_ position: Full.Position, in color: Color) -> [UInt8] { + var payload: [UInt8] = [] + + payload.append(contentsOf: [ + position.rawValue, + ]) + + payload.append(contentsOf: color.robot) + payload.append(payload.checksum8) + + payload.insert(Full.id, at: 0) + + return payload + } + + private func shineRange(from start: UInt8, to end: UInt8, in color: Color) -> [UInt8] { + var payload: [UInt8] = [] + + payload.append(contentsOf: [ + Range.Position.belt.rawValue, + start, + end, + ]) + + payload.append(contentsOf: color.robot) + payload.append(payload.checksum8) + + payload.insert(Range.id, at: 0) + + return payload + } + } + + func shine(_ lights: Lights) { + log.trace("🤖 SHINE \(lights)") + + let output = Self.commandGenerator(commands: lights.cmd) + + connectedPeripheral? + .sendCommand(output) + } + + func blacken(_ lights: Lights) { + log.trace("🤖 BLACKEN \(lights)") + switch lights { + case .all: + self.shine(.all(in: .black)) + case let .full(position, _): + self.shine(.full(position, in: .black)) + case .halfLeft: + self.shine(.halfLeft(in: .black)) + case .halfRight: + self.shine(.halfRight(in: .black)) + case .quarterFrontLeft: + self.shine(.quarterFrontLeft(in: .black)) + case .quarterFrontRight: + self.shine(.quarterFrontRight(in: .black)) + case .quarterBackLeft: + self.shine(.quarterBackLeft(in: .black)) + case .quarterBackRight: + self.shine(.quarterBackRight(in: .black)) + case .earLeft: + self.shine(.earLeft(in: .black)) + case .earRight: + self.shine(.earRight(in: .black)) + case let .spot(position, ids, _): + self.shine(.spot(position, ids: ids, in: .black)) + case let .range(start, end, _): + self.shine(.range(start: start, end: end, in: .black)) + } + } + + func blacken(_ lights: Lights.Blacken) { + log.trace("🤖 BLACKEN \(lights)") + switch lights { + case .all: + self.shine(.all(in: .black)) + case let .full(position): + self.shine(.full(position, in: .black)) + case .halfLeft: + self.shine(.halfLeft(in: .black)) + case .halfRight: + self.shine(.halfRight(in: .black)) + case .quarterFrontLeft: + self.shine(.quarterFrontLeft(in: .black)) + case .quarterFrontRight: + self.shine(.quarterFrontRight(in: .black)) + case .quarterBackLeft: + self.shine(.quarterBackLeft(in: .black)) + case .quarterBackRight: + self.shine(.quarterBackRight(in: .black)) + case .earLeft: + self.shine(.earLeft(in: .black)) + case .earRight: + self.shine(.earRight(in: .black)) + case let .spot(postion, ids): + self.shine(.spot(postion, ids: ids, in: .black)) + case let .range(start, end): + self.shine(.range(start: start, end: end, in: .black)) + } + } + + func stopLights() { + log.trace("🤖 STOP 🛑 - Lights") + self.shine(.all(in: .black)) + } +} + +// swiftlint:enable identifier_name nesting cyclomatic_complexity diff --git a/Modules/RobotKit/Sources/Robot+Motion.swift b/Modules/RobotKit/Sources/Robot+Motion.swift new file mode 100644 index 0000000000..7555708317 --- /dev/null +++ b/Modules/RobotKit/Sources/Robot+Motion.swift @@ -0,0 +1,161 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +// swiftlint:disable nesting + +extension Float { + var isInRange0to1: Bool { + self >= 0 && self <= 1 + } +} + +public extension Robot { + enum Motion { + case stop + case free(left: Float, right: Float) + + case forward(speed: Float) + case forwardLeft(speed: Float) + case forwardRight(speed: Float) + case backward(speed: Float) + case backwardLeft(speed: Float) + case backwardRight(speed: Float) + case spin(Rotation, speed: Float) + + // MARK: Public + + public enum Rotation: UInt8 { + case clockwise = 0x00 + case counterclockwise = 0x01 + } + + // MARK: Internal + + enum Motor: UInt8 { + case left = 0x21 + case right = 0x22 + } + + static let id: UInt8 = 0x20 + + var cmd: [[UInt8]] { + var output: [[UInt8]] = [[]] + + switch self { + case .stop: + let payload = [ + setMotor(.left, speed: 0, rotation: .clockwise), + setMotor(.right, speed: 0, rotation: .clockwise), + ] + output.append(contentsOf: payload) + + case let .free(left, right): + let payload = [ + setMotor(.left, speed: left, rotation: left < 0 ? .clockwise : .counterclockwise), + setMotor(.right, speed: right, rotation: right < 0 ? .clockwise : .counterclockwise), + ] + output.append(contentsOf: payload) + + case let .forward(speed): + guard speed.isInRange0to1 else { break } + let payload = [ + setMotor(.left, speed: speed, rotation: .counterclockwise), + setMotor(.right, speed: speed, rotation: .counterclockwise), + ] + output.append(contentsOf: payload) + + case let .forwardLeft(speed): + guard speed.isInRange0to1 else { break } + let payload = [ + setMotor(.left, speed: speed * 0.8, rotation: .counterclockwise), + setMotor(.right, speed: speed, rotation: .counterclockwise), + ] + output.append(contentsOf: payload) + + case let .forwardRight(speed): + guard speed.isInRange0to1 else { break } + let payload = [ + setMotor(.left, speed: speed, rotation: .counterclockwise), + setMotor(.right, speed: speed * 0.8, rotation: .counterclockwise), + ] + output.append(contentsOf: payload) + + case let .backward(speed): + guard speed.isInRange0to1 else { break } + let payload = [ + setMotor(.left, speed: speed, rotation: .clockwise), + setMotor(.right, speed: speed, rotation: .clockwise), + ] + output.append(contentsOf: payload) + + case let .backwardLeft(speed): + guard speed.isInRange0to1 else { break } + let payload = [ + setMotor(.left, speed: speed * 0.8, rotation: .clockwise), + setMotor(.right, speed: speed, rotation: .clockwise), + ] + output.append(contentsOf: payload) + + case let .backwardRight(speed): + guard speed.isInRange0to1 else { break } + let payload = [ + setMotor(.left, speed: speed, rotation: .clockwise), + setMotor(.right, speed: speed * 0.8, rotation: .clockwise), + ] + output.append(contentsOf: payload) + + case let .spin(rotation, speed): + guard speed.isInRange0to1 else { break } + let payload = [ + setMotor( + .left, speed: speed, rotation: rotation == .clockwise ? .counterclockwise : .clockwise + ), + setMotor( + .right, speed: speed, rotation: rotation == .clockwise ? .clockwise : .counterclockwise + ), + ] + output.append(contentsOf: payload) + } + + return output + } + + // MARK: Private + + private func setMotor(_ motor: Motor, speed: Float, rotation: Rotation) -> [UInt8] { + let speed = UInt8(abs(speed) * 255) + + var payload: [UInt8] = [] + + payload.append(contentsOf: [ + motor.rawValue, + rotation.rawValue, + speed, + ]) + + payload.append(payload.checksum8) + + payload.insert(Motion.id, at: 0) + + return payload + } + } + + func move(_ motion: Motion) { + log.trace("🤖 MOVE \(motion)") + let output = Self.commandGenerator(commands: motion.cmd) + + connectedPeripheral? + .sendCommand(output) + } + + func stopMotion() { + log.trace("🤖 STOP 🛑 - Motion") + self.move(.stop) + } +} + +// swiftlint:enable nesting diff --git a/Modules/RobotKit/Sources/Robot+Reinforcers+Codable.swift b/Modules/RobotKit/Sources/Robot+Reinforcers+Codable.swift new file mode 100644 index 0000000000..98f17f4c52 --- /dev/null +++ b/Modules/RobotKit/Sources/Robot+Reinforcers+Codable.swift @@ -0,0 +1,53 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +extension Robot.Reinforcer: Codable { + var stringValue: String { + switch self { + case .rainbow: "rainbow" + case .fire: "fire" + case .sprinkles: "sprinkles" + case .spinBlinkBlueViolet: "spinBlinkBlueViolet" + case .spinBlinkGreenOff: "spinBlinkGreenOff" + } + } + + init(stringValue: String) { + switch stringValue { + case "rainbow": self = .rainbow + case "fire": self = .fire + case "sprinkles": self = .sprinkles + case "spinBlinkBlueViolet": self = .spinBlinkBlueViolet + case "spinBlinkGreenOff": self = .spinBlinkGreenOff + default: self = .rainbow + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let decodedStringValue = try container.decode(String.self) + + let value = Robot.Reinforcer(stringValue: decodedStringValue) + + self = value + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.stringValue) + } + + public var image: UIImage { + switch self { + case .spinBlinkBlueViolet: DesignKitAsset.Reinforcers.spinBlinkBlueViolet.image + case .fire: DesignKitAsset.Reinforcers.fire.image + case .sprinkles: DesignKitAsset.Reinforcers.sprinkles.image + case .rainbow: DesignKitAsset.Reinforcers.rainbow.image + default: DesignKitAsset.Reinforcers.spinBlinkGreenOff.image + } + } +} diff --git a/Modules/RobotKit/Sources/Robot+Reinforcers.swift b/Modules/RobotKit/Sources/Robot+Reinforcers.swift new file mode 100644 index 0000000000..1edc3dd915 --- /dev/null +++ b/Modules/RobotKit/Sources/Robot+Reinforcers.swift @@ -0,0 +1,36 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +public extension Robot { + enum Reinforcer: UInt8, CaseIterable { + case rainbow = 0x51 + case fire = 0x52 + case sprinkles = 0x53 + case spinBlinkBlueViolet = 0x54 + case spinBlinkGreenOff = 0x55 + + // MARK: Internal + + static let id: UInt8 = 0x50 + + var cmd: [UInt8] { + let output: [UInt8] = [ + Self.id, + rawValue, + [rawValue].checksum8, + ] + + return output + } + } + + func run(_ reinforcer: Reinforcer) { + log.trace("🤖 RUN reinforcer \(reinforcer)") + + let output = Self.commandGenerator(commands: reinforcer.cmd) + + connectedPeripheral? + .sendCommand(output) + } +} diff --git a/Modules/RobotKit/Sources/Robot.swift b/Modules/RobotKit/Sources/Robot.swift new file mode 100644 index 0000000000..5f95778ee6 --- /dev/null +++ b/Modules/RobotKit/Sources/Robot.swift @@ -0,0 +1,74 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import Combine +import Foundation +import LogKit +import Version + +let log = LogKit.createLoggerFor(module: "RobotKit") + +// MARK: - Robot + +public class Robot { + // MARK: Lifecycle + + private init() { + subscribeToBLEConnectionUpdates() + } + + // MARK: Public + + public static var shared: Robot = .init() + + // MARK: - Information + + public var isConnected: CurrentValueSubject = CurrentValueSubject(false) + + public var name: CurrentValueSubject = CurrentValueSubject("(robot not connected)") + public var osVersion: CurrentValueSubject = CurrentValueSubject(nil) + public var serialNumber: CurrentValueSubject = CurrentValueSubject("(n/a)") + public var isCharging: CurrentValueSubject = CurrentValueSubject(false) + public var battery: CurrentValueSubject = CurrentValueSubject(0) + + // MARK: - Internal properties + + public var connectedPeripheral: RobotPeripheral? { + didSet { + registerBatteryCharacteristicNotificationCallback() + registerChargingStatusNotificationCallback() + + registerOSVersionReadCallback() + registerSerialNumberReadCallback() + registerChargingStatusReadCallback() + + self.connectedPeripheral?.discoverAndListenForUpdates() + self.connectedPeripheral?.readReadOnlyCharacteristics() + } + } + + // MARK: - General + + public func stop() { + log.trace("🤖 STOP 🛑 - Everything") + stopLights() + stopMotion() + } + + public func reboot() { + log.trace("🤖 REBOOT 💫") + } + + // MARK: - Magic Cards + + public func onMagicCard() -> AnyPublisher { + Just(MagicCard.dice_roll) + .eraseToAnyPublisher() + } + + // MARK: Internal + + var cancellables: Set = [] +} diff --git a/Modules/RobotKit/Sources/UI/Buttons/ButtonBordered.swift b/Modules/RobotKit/Sources/UI/Buttons/ButtonBordered.swift new file mode 100644 index 0000000000..18f4f9efba --- /dev/null +++ b/Modules/RobotKit/Sources/UI/Buttons/ButtonBordered.swift @@ -0,0 +1,66 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct ButtonBordered: View { + // MARK: Lifecycle + + init(tint: Color? = nil, @ViewBuilder label: () -> Label, action: @escaping () -> Void) { + self.foreground = tint + self.border = tint + self.action = action + self.label = label() + } + + init(foreground: Color, border: Color, @ViewBuilder label: () -> Label, action: @escaping () -> Void) { + self.foreground = foreground + self.border = border + self.action = action + self.label = label() + } + + // MARK: Internal + + var body: some View { + Button( + action: { + self.action() + }, + label: { + self.label + } + ) + .buttonStyle(.robotControlBorderedButtonStyle(foreground: self.foreground, border: self.border)) + } + + // MARK: Private + + private let foreground: Color? + private let border: Color? + private let label: Label + private let action: () -> Void +} + +#Preview { + VStack { + ButtonBordered { + Text("Hello, World") + } action: { + print("Button bordered pressed!") + } + + ButtonBordered(tint: .orange) { + Text("Hello, World") + } action: { + print("Button bordered pressed!") + } + + ButtonBordered(foreground: .red, border: .green) { + Text("Hello, World") + } action: { + print("Button bordered pressed!") + } + } +} diff --git a/Modules/RobotKit/Sources/UI/Buttons/ButtonFilled.swift b/Modules/RobotKit/Sources/UI/Buttons/ButtonFilled.swift new file mode 100644 index 0000000000..ef49286104 --- /dev/null +++ b/Modules/RobotKit/Sources/UI/Buttons/ButtonFilled.swift @@ -0,0 +1,67 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +struct ButtonFilled: View { + // MARK: Lifecycle + + init(foreground: Color, background: Color, @ViewBuilder label: () -> Label, action: @escaping () -> Void) { + self.foreground = foreground + self.background = background + self.action = action + self.label = label() + } + + init(tint: Color? = nil, @ViewBuilder label: () -> Label, action: @escaping () -> Void) { + self.foreground = .white + self.background = tint ?? .accentColor + self.action = action + self.label = label() + } + + // MARK: Internal + + var body: some View { + Button( + action: { + self.action() + }, + label: { + self.label + } + ) + .buttonStyle(.robotControlPlainButtonStyle(foreground: self.foreground, background: self.background)) + } + + // MARK: Private + + private let foreground: Color + private let background: Color + private let label: Label + private let action: () -> Void +} + +#Preview { + VStack { + ButtonFilled { + Image(systemName: "hand.wave.fill") + Text("Hello, World") + } action: { + print("Button filled pressed!") + } + + ButtonFilled(tint: .orange) { + Text("Hello, World") + } action: { + print("Button filled pressed!") + } + + ButtonFilled(foreground: .red, background: .yellow) { + Text("Hello, World") + } action: { + print("Button filled pressed!") + } + } +} diff --git a/Modules/RobotKit/Sources/UI/Buttons/ButtonStyle+Extension.swift b/Modules/RobotKit/Sources/UI/Buttons/ButtonStyle+Extension.swift new file mode 100644 index 0000000000..1c52ab8b70 --- /dev/null +++ b/Modules/RobotKit/Sources/UI/Buttons/ButtonStyle+Extension.swift @@ -0,0 +1,93 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +// + +// MARK: - Public ButtonStyle extensions + +// + +public extension ButtonStyle where Self == RobotControlPlainButtonStyle { + static func robotControlPlainButtonStyle(foreground: Color? = nil, background: Color? = nil) -> Self { + .init(foreground: foreground, background: background) + } +} + +public extension ButtonStyle where Self == RobotControlBorderedButtonStyle { + static func robotControlBorderedButtonStyle(foreground: Color? = nil, border: Color? = nil) -> Self { + .init(foreground: foreground, border: border) + } +} + +// MARK: - RobotControlPlainButtonStyle + +// + +// + +public struct RobotControlPlainButtonStyle: ButtonStyle { + // MARK: Lifecycle + + init(foreground: Color?, background: Color?) { + self.foreground = foreground ?? .black + self.background = background ?? .white + } + + // MARK: Public + + public func makeBody(configuration: Configuration) -> some View { + HStack(spacing: 5) { + configuration.label + } + .padding(.vertical, 7) + .padding(.horizontal, 20) + .foregroundColor(self.foreground) + .background(self.background) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .opacity(configuration.isPressed ? 0.8 : 1) + } + + // MARK: Private + + private let foreground: Color + private let background: Color +} + +// MARK: - RobotControlBorderedButtonStyle + +public struct RobotControlBorderedButtonStyle: ButtonStyle { + // MARK: Lifecycle + + init(foreground: Color?, border: Color?, background: Color? = nil) { + self.foreground = foreground ?? .accentColor + self.border = border ?? .accentColor + self.background = background ?? .clear + } + + // MARK: Public + + public func makeBody(configuration: Configuration) -> some View { + HStack(spacing: 5) { + configuration.label + } + .padding(.vertical, 7) + .padding(.horizontal, 20) + .foregroundColor(self.foreground) + .background(self.background) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .strokeBorder(self.border, lineWidth: 2) + ) + .opacity(configuration.isPressed ? 0.8 : 1) + } + + // MARK: Private + + private let foreground: Color + private let border: Color + private let background: Color +} diff --git a/Modules/RobotKit/Sources/UI/Buttons/Buttons.swift b/Modules/RobotKit/Sources/UI/Buttons/Buttons.swift new file mode 100644 index 0000000000..12341615d9 --- /dev/null +++ b/Modules/RobotKit/Sources/UI/Buttons/Buttons.swift @@ -0,0 +1,43 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SwiftUI + +public struct RobotControlActionButton: View { + // MARK: Lifecycle + + public init(title: String, image: String, tint: Color, action: @escaping () -> Void) { + self.title = title + self.image = image + self.tint = tint + self.action = action + } + + // MARK: Public + + public var body: some View { + Button { + self.action() + } label: { + Image(systemName: self.image) + Text(self.title) + } + .buttonStyle(.robotControlBorderedButtonStyle(foreground: self.tint, border: self.tint)) + } + + // MARK: Private + + private let title: String + private let image: String + private let tint: Color + private let action: () -> Void +} + +#Preview { + VStack { + RobotControlActionButton(title: "Say hello", image: "ellipsis.message", tint: .teal) { + print("Hello!") + } + } +} diff --git a/Modules/RobotKit/Sources/UI/ViewModels/BatteryViewModel.swift b/Modules/RobotKit/Sources/UI/ViewModels/BatteryViewModel.swift new file mode 100644 index 0000000000..36f485469f --- /dev/null +++ b/Modules/RobotKit/Sources/UI/ViewModels/BatteryViewModel.swift @@ -0,0 +1,43 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import SwiftUI + +public struct BatteryViewModel: Equatable { + // MARK: Lifecycle + + public init(level: Int) { + self.level = level + switch level { + case 0..<10: + self.name = "battery.0" + self.color = .red + case 10..<25: + self.name = "battery.25" + self.color = .red + case 25..<45: + self.name = "battery.25" + self.color = .orange + case 45..<70: + self.name = "battery.50" + self.color = .yellow + case 70..<95: + self.name = "battery.75" + self.color = .green + case 95...100: + self.name = "battery.100" + self.color = .green + default: + self.name = "battery.0" + self.color = .gray + } + } + + // MARK: Public + + public let level: Int + public let name: String + public let color: Color +} diff --git a/Modules/RobotKit/Sources/UI/ViewModels/ConnectedRobotInformationViewModel.swift b/Modules/RobotKit/Sources/UI/ViewModels/ConnectedRobotInformationViewModel.swift new file mode 100644 index 0000000000..4722bb0b80 --- /dev/null +++ b/Modules/RobotKit/Sources/UI/ViewModels/ConnectedRobotInformationViewModel.swift @@ -0,0 +1,89 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import SwiftUI + +public class ConnectedRobotInformationViewModel: ObservableObject { + // MARK: Lifecycle + + public init() { + self.getRobotInformation() + } + + // MARK: Public + + @Published public var isNotConnected: Bool = true + + @Published public var name: String = "(n/a)" + @Published public var serialNumber: String = "(n/a)" + @Published public var osVersion: String = "(n/a)" + + @Published public var battery: Int = 0 + @Published public var isCharging: Bool = false + + @Published public var isConnected: Bool = false { + didSet { + self.isNotConnected = !self.isConnected + } + } + + // MARK: Internal + + let robot = Robot.shared + + // MARK: Private + + private var cancellables: Set = [] + + private func getRobotInformation() { + self.robot.isConnected + .receive(on: DispatchQueue.main) + .sink { isConnected in + self.isConnected = isConnected + guard self.isNotConnected else { return } + self.name = "(not connected)" + self.serialNumber = "" + self.osVersion = "" + self.battery = 0 + self.isCharging = false + } + .store(in: &self.cancellables) + + self.robot.name + .receive(on: DispatchQueue.main) + .sink { + self.name = $0 + } + .store(in: &self.cancellables) + + self.robot.osVersion + .receive(on: DispatchQueue.main) + .sink { + self.osVersion = $0?.description ?? "(n/a)" + } + .store(in: &self.cancellables) + + self.robot.battery + .receive(on: DispatchQueue.main) + .sink { + self.battery = $0 + } + .store(in: &self.cancellables) + + self.robot.isCharging + .receive(on: DispatchQueue.main) + .sink { + self.isCharging = $0 + } + .store(in: &self.cancellables) + + self.robot.serialNumber + .receive(on: DispatchQueue.main) + .sink { + self.serialNumber = $0 + } + .store(in: &self.cancellables) + } +} diff --git a/Modules/RobotKit/Sources/UI/ViewModels/Mocks/RobotConnectionViewModel+Mocks.swift b/Modules/RobotKit/Sources/UI/ViewModels/Mocks/RobotConnectionViewModel+Mocks.swift new file mode 100644 index 0000000000..ac622a822b --- /dev/null +++ b/Modules/RobotKit/Sources/UI/ViewModels/Mocks/RobotConnectionViewModel+Mocks.swift @@ -0,0 +1,22 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +public extension RobotConnectionViewModel { + static func mock() -> RobotConnectionViewModel { + let viewModel = RobotConnectionViewModel() + viewModel.robotDiscoveries = [ + .mock(), + .mock(), + .mock(), + .mock(), + .mock(), + .mock(), + .mock(), + .mock(), + ] + return viewModel + } +} diff --git a/Modules/RobotKit/Sources/UI/ViewModels/Mocks/RobotDiscoveryViewModel+Mocks.swift b/Modules/RobotKit/Sources/UI/ViewModels/Mocks/RobotDiscoveryViewModel+Mocks.swift new file mode 100644 index 0000000000..0e8ad8730b --- /dev/null +++ b/Modules/RobotKit/Sources/UI/ViewModels/Mocks/RobotDiscoveryViewModel+Mocks.swift @@ -0,0 +1,33 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +// TODO(@ladislas): move to UtilsKit frameworks +public extension String { + static func random(length: Int) + -> String + { + let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + return String((0.. RobotDiscoveryViewModel { + .init( + name: name, + battery: battery, + isCharging: isCharging, + osVersion: osVersion, + status: status + ) + } +} diff --git a/Modules/RobotKit/Sources/UI/ViewModels/RobotConnectionViewModel.swift b/Modules/RobotKit/Sources/UI/ViewModels/RobotConnectionViewModel.swift new file mode 100644 index 0000000000..8d7354584d --- /dev/null +++ b/Modules/RobotKit/Sources/UI/ViewModels/RobotConnectionViewModel.swift @@ -0,0 +1,91 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import Combine +import Foundation + +public class RobotConnectionViewModel: ObservableObject { + // MARK: Lifecycle + + public init() { + self.connected = self.bleManager.isConnected + } + + // MARK: Public + + public func select(discovery: RobotDiscoveryModel) { + if self.selectedDiscovery == discovery { + self.selectedDiscovery = nil + log.trace("Unselected: \(discovery.id)") + return + } + + self.selectedDiscovery = discovery + log.trace("Selected: \(discovery.id)") + } + + public func scanForRobots() { + log.info("🔵 BLE - Start scanning for robots") + self.scanCancellable = self.bleManager.scanForRobots() + .receive(on: DispatchQueue.main) + .sink { _ in + // nothing to do + } receiveValue: { [weak self] discoveries in + guard let self else { return } + self.robotDiscoveries = discoveries + log.trace("🔵 BLE - Discoveries found: \(discoveries)") + } + } + + public func stopScanning() { + log.info("🔵 BLE - Stop scanning for robots") + self.scanCancellable = nil + } + + public func connectToRobot() { + guard let discovery = selectedDiscovery else { + return + } + self.bleManager.connect(discovery) + .receive(on: DispatchQueue.main) + .sink { _ in + // nothing to do + } receiveValue: { [weak self] peripheral in + guard let self else { return } + self.robot.connectedPeripheral = peripheral + self.connectedDiscovery = discovery + self.selectedDiscovery = nil + log.info("🔵 BLE - Connected to \(self.robot.name.value)") + } + .store(in: &self.cancellables) + } + + public func disconnectFromRobot() { + log.info("🔵 BLE - Disconnecting from \(self.robot.name.value)") + self.bleManager.disconnect() + self.connectedDiscovery = nil + } + + // MARK: Internal + + @Published var robotDiscoveries: [RobotDiscoveryModel] = [] + @Published var selectedDiscovery: RobotDiscoveryModel? + + @Published var connected: Bool = false + + @Published var connectedDiscovery: RobotDiscoveryModel? { + didSet { + self.connected = self.connectedDiscovery != nil + } + } + + // MARK: Private + + private let robot = Robot.shared + private let bleManager = BLEManager.shared + + private var cancellables: Set = [] + private var scanCancellable: AnyCancellable? +} diff --git a/Modules/RobotKit/Sources/UI/ViewModels/RobotDiscoveryViewModel.swift b/Modules/RobotKit/Sources/UI/ViewModels/RobotDiscoveryViewModel.swift new file mode 100644 index 0000000000..7715598939 --- /dev/null +++ b/Modules/RobotKit/Sources/UI/ViewModels/RobotDiscoveryViewModel.swift @@ -0,0 +1,57 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import SwiftUI + +// MARK: - RobotDiscoveryViewModel + +public struct RobotDiscoveryViewModel: Identifiable { + // MARK: Lifecycle + + init( + name: String, battery: Int, isCharging: Bool, osVersion: String, status: Status = .unselected + ) { + self.id = UUID() + self.name = name + self.status = status + self.isCharging = isCharging + self.osVersion = "LekaOS \(osVersion)" + self.battery = BatteryViewModel(level: battery) + } + + init(discovery: RobotDiscoveryModel, status: Status = .unselected) { + self.id = discovery.id + self.name = discovery.name + self.status = status + self.isCharging = discovery.isCharging + self.osVersion = "LekaOS \(discovery.osVersion)" + self.battery = BatteryViewModel(level: discovery.battery) + } + + // MARK: Public + + public enum Status: CaseIterable { + case connected + case unselected + case selected + } + + public let id: UUID + public let name: String + public let status: Status + public let isCharging: Bool + public let osVersion: String + public let battery: BatteryViewModel +} + +// MARK: Equatable + +extension RobotDiscoveryViewModel: Equatable { + public static func == (lhs: RobotDiscoveryViewModel, rhs: RobotDiscoveryViewModel) -> Bool { + lhs.id == rhs.id + && lhs.isCharging == rhs.isCharging + && lhs.battery == rhs.battery + } +} diff --git a/Modules/RobotKit/Sources/UI/Views/RobotConnectionView+l10n.swift b/Modules/RobotKit/Sources/UI/Views/RobotConnectionView+l10n.swift new file mode 100644 index 0000000000..c361c60045 --- /dev/null +++ b/Modules/RobotKit/Sources/UI/Views/RobotConnectionView+l10n.swift @@ -0,0 +1,57 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import LocalizationKit + +// swiftlint:disable nesting + +extension l10n { + enum RobotKit { + enum RobotConnectionView { + static let navigationTitle = LocalizedString( + "robotkit.robot_connect_view.navigation_title", + bundle: RobotKitResources.bundle, + value: "Connect to a robot", + comment: "The title of the robot connection view" + ) + + static let searchingViewText = LocalizedString( + "robotkit.robot_connect_view.searching_view_text", + bundle: RobotKitResources.bundle, + value: "Searching for robots...", + comment: "The text displayed in the searching view" + ) + + static let cancelButton = LocalizedString( + "robotkit.robot_connect_view.cancel_button", + bundle: RobotKitResources.bundle, + value: "Cancel", + comment: "The title of the cancel button in the toolbar" + ) + + static let connectButton = LocalizedString( + "robotkit.robot_connect_view.connect_button", + bundle: RobotKitResources.bundle, + value: "Connect", + comment: "The title of the connect button" + ) + + static let disconnectButton = LocalizedString( + "robotkit.robot_connect_view.disconnect_button", + bundle: RobotKitResources.bundle, + value: "Disconnect", + comment: "The title of the disconnect button" + ) + + static let continueButton = LocalizedString( + "robotkit.robot_connect_view.continue_button", + bundle: RobotKitResources.bundle, + value: "Continue", + comment: "The title of the continue button" + ) + } + } +} + +// swiftlint:enable nesting diff --git a/Modules/RobotKit/Sources/UI/Views/RobotConnectionView.swift b/Modules/RobotKit/Sources/UI/Views/RobotConnectionView.swift new file mode 100644 index 0000000000..339c6d3863 --- /dev/null +++ b/Modules/RobotKit/Sources/UI/Views/RobotConnectionView.swift @@ -0,0 +1,193 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import Combine +import DesignKit +import LocalizationKit +import SwiftUI + +public struct RobotConnectionView: View { + // MARK: Lifecycle + + public init(viewModel: RobotConnectionViewModel = RobotConnectionViewModel()) { + _viewModel = StateObject(wrappedValue: viewModel) + } + + // MARK: Public + + public var body: some View { + VStack(spacing: 10) { + switch self.viewModel.robotDiscoveries.count { + case 0: + Spacer() + self.searchingView + Spacer() + default: + self.robotDiscoveryGridView + } + + Divider() + .padding(.horizontal) + .padding(.horizontal) + + HStack { + if !self.viewModel.connected { + self.connectButton + } else { + self.disconnectButton + } + self.continueButton + } + .padding(.top, 15) + .padding(.bottom, 40) + } + .background(.lkBackground) + .onAppear { + self.viewModel.scanForRobots() + } + .onDisappear { + self.viewModel.stopScanning() + } + .navigationTitle(String(l10n.RobotKit.RobotConnectionView.navigationTitle.characters)) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + self.dismiss() + } label: { + Text(l10n.RobotKit.RobotConnectionView.cancelButton) + } + } + } + } + + // MARK: Internal + + @StateObject var viewModel: RobotConnectionViewModel + + @Environment(\.dismiss) var dismiss + + // MARK: Private + + private let columns: [GridItem] = [ + GridItem(), + GridItem(), + GridItem(), + ] + + private var searchingView: some View { + // TODO(@ladislas): review "no robot found" interface + // TODO(@ladislas): handle no robots found after xx seconds + add refresh button + VStack { + Text(l10n.RobotKit.RobotConnectionView.searchingViewText) + ProgressView() + } + } + + private var robotDiscoveryGridView: some View { + ScrollView(.vertical, showsIndicators: true) { + LazyVGrid(columns: self.columns, spacing: 40) { + ForEach(self.viewModel.robotDiscoveries) { discovery in + self.robotDiscoveryCellView(for: discovery) + .contentShape(Rectangle()) + .onTapGesture { + withAnimation { + self.viewModel.select(discovery: discovery) + } + } + } + } + } + } + + private var connectButton: some View { + ButtonBordered(tint: .green) { + HStack { + Image(systemName: "checkmark.circle") + Text(l10n.RobotKit.RobotConnectionView.connectButton) + } + .frame(minWidth: 200) + } action: { + withAnimation { + self.viewModel.connectToRobot() + } + } + .disabled(self.viewModel.selectedDiscovery == nil) + } + + private var disconnectButton: some View { + ButtonBordered(tint: .orange) { + HStack { + Image(systemName: "xmark.circle") + Text(l10n.RobotKit.RobotConnectionView.disconnectButton) + } + .frame(minWidth: 200) + } action: { + let animation = Animation.easeOut(duration: 0.5) + withAnimation(animation) { + self.viewModel.disconnectFromRobot() + } + } + } + + @ViewBuilder + private var continueButton: some View { + if self.viewModel.connected { + ButtonFilled(tint: .green) { + HStack { + Image(systemName: "arrow.right.circle") + Text(l10n.RobotKit.RobotConnectionView.continueButton) + } + .frame(minWidth: 200) + } action: { + self.dismiss() + } + .disabled(false) + } else { + ButtonBordered(tint: .gray) { + HStack { + Image(systemName: "arrow.right.circle") + Text(l10n.RobotKit.RobotConnectionView.continueButton) + } + .frame(minWidth: 200) + } action: { + // nothing to do + } + .disabled(true) + } + } + + @ViewBuilder + private func robotDiscoveryCellView(for discovery: RobotDiscoveryModel) -> some View { + if discovery == self.viewModel.connectedDiscovery { + RobotDiscoveryView( + discovery: RobotDiscoveryViewModel( + discovery: discovery, status: .connected + ) + ) + } else if discovery == self.viewModel.selectedDiscovery { + RobotDiscoveryView( + discovery: RobotDiscoveryViewModel( + discovery: discovery, status: .selected + ) + ) + } else { + RobotDiscoveryView( + discovery: RobotDiscoveryViewModel( + discovery: discovery, status: .unselected + ) + ) + } + } +} + +#Preview { + let viewModel: RobotConnectionViewModel = .mock() + return Text("Preview") + .sheet(isPresented: .constant(true)) { + NavigationStack { + RobotConnectionView(viewModel: viewModel) + } + } +} diff --git a/Modules/RobotKit/Sources/UI/Views/RobotDiscoveryView.swift b/Modules/RobotKit/Sources/UI/Views/RobotDiscoveryView.swift new file mode 100644 index 0000000000..c75c84adbf --- /dev/null +++ b/Modules/RobotKit/Sources/UI/Views/RobotDiscoveryView.swift @@ -0,0 +1,125 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import DesignKit +import SwiftUI + +struct RobotDiscoveryView: View { + // MARK: Lifecycle + + // MARK: - Public functions + + public init(discovery: RobotDiscoveryViewModel) { + self.discovery = discovery + } + + // MARK: Internal + + // MARK: - Views + + var body: some View { + VStack { + VStack(spacing: 30) { + self.robotFace + self.robotName + } + self.robotCharginStatusAndBattery + self.robotOsVersion + } + .padding(.top, 30) + .padding(.horizontal, 20) + } + + // MARK: Private + + // MARK: - Private variables + + @State private var rotation: CGFloat = 0.0 + @State private var inset: CGFloat = 0.0 + + private var discovery: RobotDiscoveryViewModel + + // MARK: - Private views + + private var robotFace: some View { + DesignKitAsset.Images.robotFaceSimple.swiftUIImage + .overlay(content: { + Circle() + .inset(by: -10) + .stroke( + DesignKitAsset.Colors.lekaGreen.swiftUIColor, + style: StrokeStyle( + lineWidth: 2, + lineCap: .butt, + lineJoin: .round, + dash: [12, 3] + ) + ) + .opacity(self.discovery.status == .selected ? 1 : 0) + .rotationEffect(.degrees(self.rotation), anchor: .center) + .animation( + Animation + .linear(duration: 15) + .repeatForever(autoreverses: false), + value: self.rotation + ) + .onAppear { + self.rotation = 360 + } + }) + .onAppear { + guard self.discovery.status == .connected else { return } + let baseAnimation = Animation.bouncy(duration: 0.5) + withAnimation(baseAnimation) { + self.inset = -26.0 + } + } + .background( + DesignKitAsset.Colors.lekaGreen.swiftUIColor.opacity(self.discovery.status == .connected ? 1.0 : 0.0), + in: Circle().inset(by: self.inset) + ) + } + + private var robotName: some View { + Text(self.discovery.name) + .font(.title3) + .multilineTextAlignment(.center) + .lineLimit(1) + } + + private var robotCharginStatusAndBattery: some View { + HStack { + if self.discovery.isCharging { + Image(systemName: "bolt.circle.fill") + .foregroundColor(.blue) + } else { + Image(systemName: "bolt.slash.circle") + .foregroundColor(.gray.opacity(0.6)) + } + + HStack(spacing: 5) { + Image(systemName: self.discovery.battery.name) + .foregroundColor(self.discovery.battery.color) + Text("\(self.discovery.battery.level)%") + .foregroundColor(.gray) + } + } + } + + private var robotOsVersion: some View { + Text(self.discovery.osVersion) + .font(.caption) + .foregroundColor(.gray) + } +} + +#Preview { + HStack(spacing: 100) { + RobotDiscoveryView(discovery: .mock(name: "Leka unselected", status: .unselected)) + + RobotDiscoveryView(discovery: .mock(name: "Leka selected", status: .selected)) + + RobotDiscoveryView(discovery: .mock(name: "Leka connected", status: .connected)) + } +} diff --git a/Modules/RobotKit/Tests/Array_checksum8_Tests.swift b/Modules/RobotKit/Tests/Array_checksum8_Tests.swift new file mode 100644 index 0000000000..b61f2159ca --- /dev/null +++ b/Modules/RobotKit/Tests/Array_checksum8_Tests.swift @@ -0,0 +1,107 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +@testable import RobotKit + +final class Array_checksum8_Tests: XCTestCase { + func test_checksum8ForOneValue0x00() { + // Given + let data: [UInt8] = [0x00] + + // When + let checksum = data.checksum8 + + // Then + XCTAssertEqual(checksum, 0x00) + } + + func test_checksum8ForOneValue0xFF() { + // Given + let data: [UInt8] = [0xFF] + + // When + let checksum = data.checksum8 + + // Then + XCTAssertEqual(checksum, 0xFF) + } + + func test_checksum8ForOneValueInfoCommand() { + // Given + let data: [UInt8] = [0x70] + + // When + let checksum = data.checksum8 + + // Then + XCTAssertEqual(checksum, 0x70) + } + + func test_checksum8ForTwoValues() { + if true { + // Given + let data: [UInt8] = [0x00, 0xFF] + + // When + let checksum = data.checksum8 + + // Then + XCTAssertEqual(checksum, 0xFF) + } + + if true { + // Given + let data: [UInt8] = [0x01, 0xFF] + + // When + let checksum = data.checksum8 + + // Then + XCTAssertEqual(checksum, 0x00) + } + + if true { + // Given + let data: [UInt8] = [0xFF, 0x01] + + // When + let checksum = data.checksum8 + + // Then + XCTAssertEqual(checksum, 0x00) + } + } + + func test_checksum8ForMultipleValuesTurnOneLedOn() { + // Given + let data: [UInt8] = [0x15, 0x00, 0xFF, 0x00, 0x00] + + // When + let checksum = data.checksum8 + + // Then + XCTAssertEqual(checksum, 0x14) + } + + func test_checksum8ForMultipleValuesTurnAllLedsOn() { + // Given + let data: [UInt8] = [ + 0x15, 0x00, 0x33, 0x00, 0x00, 0x15, 0x01, 0x66, 0x00, 0x00, 0x15, 0x02, 0x99, 0x00, 0x00, 0x15, 0x03, + 0xCC, 0x00, 0x00, 0x15, 0x04, 0xFF, 0x00, 0x00, 0x15, 0x05, 0x00, 0x00, 0x00, 0x15, 0x06, 0x00, 0x33, + 0x00, 0x15, 0x07, 0x00, 0x66, 0x00, 0x15, 0x08, 0x00, 0x99, 0x00, 0x15, 0x09, 0x00, 0xCC, 0x00, 0x15, + 0x0A, 0x00, 0xFF, 0x00, 0x15, 0x0B, 0x00, 0x00, 0x00, 0x15, 0x0C, 0x00, 0x00, 0x33, 0x15, 0x0D, 0x00, + 0x00, 0x66, 0x15, 0x0E, 0x00, 0x00, 0x99, 0x15, 0x0F, 0x00, 0x00, 0xCC, 0x15, 0x10, 0xFF, 0x00, 0x00, + 0x15, 0x11, 0x00, 0xFF, 0x00, 0x15, 0x12, 0x00, 0x00, 0xFF, 0x15, 0x13, 0xFF, 0xFF, 0xFF, + ] + + // When + let checksum = data.checksum8 + + // Then + XCTAssertEqual(checksum, 0x54) + } +} diff --git a/Modules/RobotKit/Tests/MagicCard_Tests.swift b/Modules/RobotKit/Tests/MagicCard_Tests.swift new file mode 100644 index 0000000000..ac3f16905a --- /dev/null +++ b/Modules/RobotKit/Tests/MagicCard_Tests.swift @@ -0,0 +1,86 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import XCTest + +@testable import RobotKit + +final class MagicCard_Tests: XCTestCase { + func test_shouldBeLooselyEqualWhenLanguagesAreTheSame() { + // Given + + // When + let card1 = MagicCard(language: .fr_FR, id: 0xBEEF) + let card2 = MagicCard(language: .fr_FR, id: 0xBEEF) + + // Then + XCTAssertTrue(card1 == card2) + } + + func test_shouldBeLooselyEqualWhenLanguagesAreNotTheSame() { + // Given + + // When + let card1 = MagicCard(language: .fr_FR, id: 0xBEEF) + let card2 = MagicCard(language: .en_US, id: 0xBEEF) + + // Then + XCTAssertTrue(card1 == card2) + } + + func test_shouldBeStrictlyEqualWhenLanguagesAreTheSame() { + // Given + + // When + let card1 = MagicCard(language: .fr_FR, id: 0xBEEF) + let card2 = MagicCard(language: .fr_FR, id: 0xBEEF) + + // Then + XCTAssertTrue(card1 === card2) + } + + func test_shouldNotBeStrictlyEqualWhenLanguagesAreNotTheSame() { + // Given + + // When + let card1 = MagicCard(language: .fr_FR, id: 0xBEEF) + let card2 = MagicCard(language: .en_US, id: 0xBEEF) + + // Then + XCTAssertFalse(card1 === card2) + } + + func test_shouldBeLooselyEqualToAvailableCard() { + // Given + + // When + let card1 = MagicCard(id: 0x0002) + let card2 = MagicCard.dice_roll + + // Then + XCTAssertTrue(card1 == card2) + } + + func test_shouldBeLooselyEqualWithDifferentLanguageToAvailableCard() { + // Given + + // When + let card1 = MagicCard(language: .fr_FR, id: 0x0002) + let card2 = MagicCard.dice_roll + + // Then + XCTAssertTrue(card1 == card2) + } + + func test_shouldNotBeStrictlyEqualWithDifferentLanguageToAvailableCard() { + // Given + + // When + let card1 = MagicCard(language: .fr_FR, id: 0x0002) + let card2 = MagicCard.dice_roll + + // Then + XCTAssertFalse(card1 === card2) + } +} diff --git a/Modules/RobotKit/Tests/RobotKit_Tests.swift b/Modules/RobotKit/Tests/RobotKit_Tests.swift new file mode 100644 index 0000000000..9c46b9cdbe --- /dev/null +++ b/Modules/RobotKit/Tests/RobotKit_Tests.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +final class RobotKit_Tests: XCTestCase { + func test_twoPlusTwo_isFour() { + XCTAssertEqual(2 + 2, 4) + } +} diff --git a/Modules/RobotKit/Tests/Robot_Commands_Tests.swift b/Modules/RobotKit/Tests/Robot_Commands_Tests.swift new file mode 100644 index 0000000000..a36d49b4d3 --- /dev/null +++ b/Modules/RobotKit/Tests/Robot_Commands_Tests.swift @@ -0,0 +1,40 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import XCTest + +@testable import RobotKit + +final class Robot_Commands_Tests: XCTestCase { + func test_generateFrameForOneCommand() { + // Given + let cmd: [UInt8] = [0x50, 0x51, 0x51] + + // When + let output = Robot.commandGenerator(commands: cmd) + let expected = Data([0x2A, 0x2A, 0x2A, 0x2A, 0x01, 0x50, 0x51, 0x51]) + + // Then + XCTAssertEqual(output, expected) + } + + func test_generateFrameForMultipleCommands() { + // Given + let cmd1: [UInt8] = [0x50, 0x51, 0x51] + let cmd2: [UInt8] = [0x60, 0x61, 0x61] + let cmd3: [UInt8] = [0x70, 0x71, 0x71] + + // When + let output = Robot.commandGenerator(commands: cmd1, cmd2, cmd3) + let expected = Data([ + 0x2A, 0x2A, 0x2A, 0x2A, 0x03, + 0x50, 0x51, 0x51, + 0x60, 0x61, 0x61, + 0x70, 0x71, 0x71, + ]) + + // Then + XCTAssertEqual(output, expected) + } +} diff --git a/Plugins/ios-monorepo/Package.swift b/Plugins/ios-monorepo/Package.swift index 258c84c7c0..8d273038c3 100644 --- a/Plugins/ios-monorepo/Package.swift +++ b/Plugins/ios-monorepo/Package.swift @@ -1,4 +1,6 @@ -// swift-tools-version: 5.4 +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 import PackageDescription diff --git a/Plugins/ios-monorepo/Plugin.swift b/Plugins/ios-monorepo/Plugin.swift index 86a1e55b85..28b384c0d3 100644 --- a/Plugins/ios-monorepo/Plugin.swift +++ b/Plugins/ios-monorepo/Plugin.swift @@ -1,3 +1,7 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + import ProjectDescription -let plugin = Plugin(name: "MyPlugin") \ No newline at end of file +let plugin = Plugin(name: "MyPlugin") diff --git a/Plugins/ios-monorepo/ProjectDescriptionHelpers/LocalHelper.swift b/Plugins/ios-monorepo/ProjectDescriptionHelpers/LocalHelper.swift index 6f49a442da..181513d3c3 100644 --- a/Plugins/ios-monorepo/ProjectDescriptionHelpers/LocalHelper.swift +++ b/Plugins/ios-monorepo/ProjectDescriptionHelpers/LocalHelper.swift @@ -1,9 +1,17 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + import Foundation public struct LocalHelper { - let name: String + // MARK: Lifecycle public init(name: String) { self.name = name } + + // MARK: Internal + + let name: String } diff --git a/Plugins/ios-monorepo/Sources/tuist-my-cli/main.swift b/Plugins/ios-monorepo/Sources/tuist-my-cli/main.swift index 6f059694c4..eb123df175 100644 --- a/Plugins/ios-monorepo/Sources/tuist-my-cli/main.swift +++ b/Plugins/ios-monorepo/Sources/tuist-my-cli/main.swift @@ -1 +1,5 @@ -print("Hello, from your Tuist Task") \ No newline at end of file +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +print("Hello, from your Tuist Task") diff --git a/README.md b/README.md index 0e857692cd..bf6a9297e9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ # Leka iOS' Monorepo -[![Tuist badge](https://img.shields.io/badge/Powered%20by-Tuist-blue)](https://tuist.io) +[![Tuist badge](https://img.shields.io/badge/Powered%20by-Tuist-blue)](https://tuist.io) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=leka_ios-monorepo&metric=alert_status&token=ae37dc9610e171e3c40c43642f1697e2e5f05db4)](https://sonarcloud.io/summary/new_code?id=leka_ios-monorepo) [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=leka_ios-monorepo&metric=code_smells&token=ae37dc9610e171e3c40c43642f1697e2e5f05db4)](https://sonarcloud.io/summary/new_code?id=leka_ios-monorepo) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=leka_ios-monorepo&metric=security_rating&token=ae37dc9610e171e3c40c43642f1697e2e5f05db4)](https://sonarcloud.io/summary/new_code?id=leka_ios-monorepo) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=leka_ios-monorepo&metric=sqale_rating&token=ae37dc9610e171e3c40c43642f1697e2e5f05db4)](https://sonarcloud.io/summary/new_code?id=leka_ios-monorepo) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=leka_ios-monorepo&metric=bugs&token=ae37dc9610e171e3c40c43642f1697e2e5f05db4)](https://sonarcloud.io/summary/new_code?id=leka_ios-monorepo) ## About -This monorepo contains everything iOS/iPadOS related. +This monorepo contains everything iOS/iPadOS/macOS related, including: + +- iPadOS apps available on the App Store +- iPadOS tools available only for internal use +- macOS apps and tools available only for internal use ## How to @@ -20,7 +24,16 @@ curl -Ls https://install.tuist.io | bash # install latest version if needed tuist update -# generate project +# install needed tools +brew upgrade && brew install fastlane swiftlint swift-format + +# sync provisioning profiles and certificates +fastlane sync_certificates + +# pull dependencies +tuist fetch + +# generate all projects tuist generate # generate specific target/project @@ -30,8 +43,31 @@ tuist generate LekaApp tuist edit ``` -See tuist documentation: https://docs.tuist.io/tutorial/get-started +See tuist documentation: + +## `TUIST_*` generation option + +Tuist allows for "generation-time configuration" (see documentation for more information: https://docs.tuist.io/guides/environment). + +We are leveraging the feature with different options: + +- `TUIST_TURN_OFF_LINTERS` - turns off SwiftLint and swift-format, useful for CI or rapid development +- `TUIST_GENERATE_MODULES_AS_FRAMEWORKS_FOR_DEBUG` - generates modules as frameworks (instead of static libraries), allowing developers to use more Xcode features such as Canvas to preview SwiftUI Views +- `TUIST_GENERATE_MAC_OS_APPS` - generates only available macOS applications + +Example command: + +```bash +TUIST_GENERATE_MODULES_AS_FRAMEWORKS_FOR_DEBUG=TRUE \ +TUIST_GENERATE_MAC_OS_APPS=TRUE \ +tuist generate +``` + +## Example projects + +Different examples apps/tools (iOS, macOS, cli, module) are available as reference. +For more information, see [`Examples`](./Examples/) directory. ## License diff --git a/Scripts/SwiftLintRunScript.sh b/Scripts/SwiftLintRunScript.sh new file mode 100755 index 0000000000..377397fcc5 --- /dev/null +++ b/Scripts/SwiftLintRunScript.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +if test -d "/opt/homebrew/bin"; then + PATH="/opt/homebrew/bin:${PATH}" +elif test -d "/usr/local/bin"; then + PATH="/usr/local/bin:${PATH}" +fi + +export PATH + +if ! command -v swiftlint &> /dev/null; then + echo "error: swiftlint not installed, download from https://github.com/realm/SwiftLint" + echo "error: or install with brew install swiftlint" + exit 1 +fi + +SCRIPT_PATH=$(realpath "$0") +SCRIPT_DIR=$(dirname "$SCRIPT_PATH") +ROOT_DIR=$(realpath "$SCRIPT_DIR/..") + +swiftlint --config $ROOT_DIR/.swiftlint.yml --reporter xcode diff --git a/Specs/jtd/activity.jtd.json b/Specs/jtd/activity.jtd.json new file mode 100644 index 0000000000..b7fb5e9188 --- /dev/null +++ b/Specs/jtd/activity.jtd.json @@ -0,0 +1,199 @@ +{ + "properties": { + "version": { + "type": "string" + }, + "uuid": { + "type": "string" + }, + "name": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "last_edited_at": { + "type": "string" + }, + "status": { + "ref": "$status" + }, + "authors": { + "elements": { + "ref": "$author" + } + }, + "skills": { + "elements": { + "ref": "$skill" + } + }, + "hmi": { + "elements": { + "ref": "$hmi" + } + }, + "types": { + "elements": { + "ref": "$type" + } + }, + "tags": { + "elements": { + "ref": "$tag" + } + }, + "locales": { + "elements": { + "ref": "$locale" + } + }, + "l10n": { + "elements": { + "ref": "$l10n" + } + }, + "exercises_payload": { + "properties": { + "options": { + "properties": { + "shuffle_exercises": { + "type": "boolean" + }, + "shuffle_groups": { + "type": "boolean" + } + } + }, + "exercise_groups": { + "elements": { + "ref": "$group" + } + } + } + } + }, + "definitions": { + "$author": { + "enum": ["leka", "aurore_kiesler", "julie_tuil"] + }, + "$hmi": { + "enum": ["robot", "tablet", "magic_cards", "tablet_robot"] + }, + "$locale": { + "enum": ["en_US", "fr_FR"] + }, + "$status": { + "enum": ["draft", "published"] + }, + "$l10n": { + "properties": { + "locale": { + "ref": "$locale" + }, + "details": { + "ref": "$l10n/details" + } + } + }, + "$l10n/details": { + "properties": { + "icon": { + "type": "string" + }, + "title": { + "type": "string" + }, + "subtitle": { + "type": "string", + "nullable": true + }, + "short_description": { + "type": "string" + }, + "description": { + "type": "string" + }, + "instructions": { + "type": "string", + "nullable": true + } + } + }, + "$skill": { + "type": "string" + }, + "$type": { + "enum": ["one_on_one", "group"] + }, + "$tag": { + "type": "string" + }, + "$group": { + "properties": { + "group": { + "elements": { + "ref": "$exercise" + } + } + } + }, + "$exercise": { + "properties": { + "instructions": { + "ref": "$exercise/instructions", + "nullable": true + }, + "interface": { + "ref": "$exercise/interface" + } + }, + "optionalProperties": { + "gameplay": { + "ref": "$exercise/gameplay", + "nullable": true + }, + "payload": { + "ref": "$exercise/payload" + }, + "action": { + "ref": "$exercise/action" + } + } + }, + "$exercise/instructions": { + "elements": { + "properties": { + "locale": { + "ref": "$locale" + }, + "value": { + "type": "string" + } + } + } + }, + "$exercise/interface": { + "enum": [ + "touchToSelect", + "robotThenTouchToSelect", + "listenThenTouchToSelect", + "observeThenTouchToSelect", + "dragAndDropIntoZones", + "dragAndDropToAssociate", + "danceFreeze", + "remoteStandard", + "remoteArrow", + "hideAndSeek", + "musicalInstruments", + "melody", + "pairing" + ] + }, + "$exercise/gameplay": { + "enum": ["findTheRightAnswers", "associateCategories"] + }, + "$exercise/action": {}, + "$exercise/payload": {} + } +} diff --git a/Specs/jtd/authors.jtd.json b/Specs/jtd/authors.jtd.json new file mode 100644 index 0000000000..0a64935a44 --- /dev/null +++ b/Specs/jtd/authors.jtd.json @@ -0,0 +1,59 @@ +{ + "properties": { + "version": { + "type": "string" + }, + "list": { + "elements": { + "ref": "$author" + } + } + }, + "definitions": { + "$locale": { + "enum": ["en_US", "fr_FR"] + }, + "$l10n": { + "properties": { + "locale": { + "ref": "$locale" + }, + "description": { + "type": "string" + } + } + }, + "$visibility": { + "enum": ["public", "private"] + }, + "$author": { + "properties": { + "id": { + "type": "string" + }, + "visible": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "website": { + "type": "string" + }, + "email": { + "type": "string" + }, + "professions": { + "elements": { + "type": "string" + } + }, + "l10n": { + "elements": { + "ref": "$l10n" + } + } + } + } + } +} diff --git a/Specs/jtd/avatars.jtd.json b/Specs/jtd/avatars.jtd.json new file mode 100644 index 0000000000..3e51a205bf --- /dev/null +++ b/Specs/jtd/avatars.jtd.json @@ -0,0 +1,50 @@ +{ + "properties": { + "version": { + "type": "string" + }, + "categories": { + "elements": { + "ref": "$category" + } + } + }, + "definitions": { + "$locale": { + "enum": ["en_US", "fr_FR"] + }, + "$l10n": { + "properties": { + "locale": { + "ref": "$locale" + }, + "name": { + "type": "string" + } + } + }, + "$avatar": { + "type": "string" + }, + "$category": { + "properties": { + "id": { + "type": "string" + }, + "visible": { + "type": "boolean" + }, + "l10n": { + "elements": { + "ref": "$l10n" + } + }, + "avatars": { + "elements": { + "ref": "$avatar" + } + } + } + } + } +} diff --git a/Specs/jtd/curriculum.jtd.json b/Specs/jtd/curriculum.jtd.json new file mode 100644 index 0000000000..67f2532a9e --- /dev/null +++ b/Specs/jtd/curriculum.jtd.json @@ -0,0 +1,101 @@ +{ + "properties": { + "uuid": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "ref": "$status" + }, + "authors": { + "elements": { + "ref": "$author" + } + }, + "skills": { + "elements": { + "ref": "$skill" + } + }, + "hmi": { + "elements": { + "ref": "$hmi" + } + }, + "tags": { + "elements": { + "ref": "$tag" + } + }, + "locales": { + "elements": { + "ref": "$locale" + } + }, + "l10n": { + "elements": { + "ref": "$l10n" + } + }, + "activities": { + "elements": { + "type": "string" + } + } + }, + "definitions": { + "$author": { + "enum": [ + "leka_at_apf_france_handicap", + "aurore_kiesler", + "julie_tuil" + ] + }, + "$hmi": { + "enum": ["robot", "tablet", "magic_cards", "tablet_robot"] + }, + "$status": { + "enum": ["draft", "published"] + }, + "$locale": { + "enum": ["en_US", "fr_FR"] + }, + "$l10n": { + "properties": { + "locale": { + "ref": "$locale" + }, + "details": { + "ref": "$l10n/details" + } + } + }, + "$l10n/details": { + "properties": { + "icon": { + "type": "string" + }, + "title": { + "type": "string" + }, + "subtitle": { + "type": "string" + }, + "abstract": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "$skill": { + "type": "string" + }, + "$tag": { + "type": "string" + } + } +} diff --git a/Specs/jtd/professions.jtd.json b/Specs/jtd/professions.jtd.json new file mode 100644 index 0000000000..742dea64d6 --- /dev/null +++ b/Specs/jtd/professions.jtd.json @@ -0,0 +1,42 @@ +{ + "properties": { + "version": { + "type": "string" + }, + "list": { + "elements": { + "ref": "$profession" + } + } + }, + "definitions": { + "$locale": { + "enum": ["en_US", "fr_FR"] + }, + "$l10n": { + "properties": { + "locale": { + "ref": "$locale" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "$profession": { + "properties": { + "id": { + "type": "string" + }, + "l10n": { + "elements": { + "ref": "$l10n" + } + } + } + } + } +} diff --git a/Specs/jtd/skills.jtd.json b/Specs/jtd/skills.jtd.json new file mode 100644 index 0000000000..a0a3966dd7 --- /dev/null +++ b/Specs/jtd/skills.jtd.json @@ -0,0 +1,47 @@ +{ + "properties": { + "version": { + "type": "string" + }, + "list": { + "elements": { + "ref": "$skill" + } + } + }, + "definitions": { + "$locale": { + "enum": ["en_US", "fr_FR"] + }, + "$l10n": { + "properties": { + "locale": { + "ref": "$locale" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "$skill": { + "properties": { + "id": { + "type": "string" + }, + "l10n": { + "elements": { + "ref": "$l10n" + } + }, + "subskills": { + "elements": { + "ref": "$skill" + } + } + } + } + } +} diff --git a/Tools/Hooks/check_xcstrings.py b/Tools/Hooks/check_xcstrings.py new file mode 100755 index 0000000000..3b51d8cc4b --- /dev/null +++ b/Tools/Hooks/check_xcstrings.py @@ -0,0 +1,59 @@ +#!/usr/bin/python3 +"""Module providing a hook to check for .xcstrings files.""" + +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +import json +import sys +from pygments import highlight +from pygments.lexers.data import JsonLexer +from pygments.formatters.terminal import TerminalFormatter + +from modules.utils import get_files +from modules.xcstrings import find_stale_entries, find_unusual_characters + + +def check_xcstrings_file(file): + """Check xcstrings for stale entries.""" + file_is_valid = True + + if stale_entries := find_stale_entries(file): + file_is_valid = False + print(f"\n❌ Stale entries found in {file}") + for key, data in stale_entries: + data = json.dumps(data, indent=4) + print(highlight(f'"{key}": {data}', JsonLexer(), TerminalFormatter())) + + if problematic_entries := find_unusual_characters(file): + file_is_valid = False + print(f"\n❌ Unusual characters found in {file}") + for key, value, character in problematic_entries: + value = json.dumps(value, indent=4) + print(f"Character: {character}") + print(highlight(f'"{key}": {value}', JsonLexer(), TerminalFormatter())) + + return file_is_valid + + +def main(): + """Main function.""" + files = get_files() + + must_fail = False + + for file in files: + file_is_valid = check_xcstrings_file(file) + + if file_is_valid is False: + must_fail = True + + if must_fail: + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Tools/Hooks/check_yaml_content_activities.py b/Tools/Hooks/check_yaml_content_activities.py new file mode 100755 index 0000000000..2774c65e65 --- /dev/null +++ b/Tools/Hooks/check_yaml_content_activities.py @@ -0,0 +1,130 @@ +#!/usr/bin/python3 +"""Check the content of a YAML file for an activity""" + +# Leka - LekaOS +# Copyright 2020 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +import sys + +from modules.utils import get_files, is_file_modified +from modules.yaml import create_yaml_object, is_jtd_schema_compliant +from modules.content import ( + is_created_at_present, + is_last_edited_at_present, + add_created_at, + add_last_edited_at, + update_last_edited_at, + is_uuid_same_as_filename, + is_name_same_as_filename, + is_uuid_valid, + find_missing_skills, + find_missing_icons, + find_string_values_starting_with_newline, + find_empty_string_values, +) + + +JTD_SCHEMA = "Specs/jtd/activity.jtd.json" + + +def check_activity(filename): + """Check the content of a YAML file for an activity""" + yaml = create_yaml_object() + + file_is_valid = True + + if is_jtd_schema_compliant(filename, JTD_SCHEMA) is False: + file_is_valid = False + + with open(filename, "r", encoding="utf8") as file: + activity = yaml.load(file) + + if differing_uuids := is_uuid_same_as_filename(activity, filename): + file_is_valid = False + activity_uuid, filename_uuid = differing_uuids + print(f"\n❌ Activity uuid and filename uuid are not the same in {filename}") + print(f"uuid: {activity_uuid}") + print(f"filename: {filename_uuid}") + + if is_uuid_valid(activity["uuid"]) is False: + file_is_valid = False + print(f"\n❌ uuid not valid in {filename}") + print(f"uuid: {activity['uuid']}") + + if differing_names := is_name_same_as_filename(activity, filename): + file_is_valid = False + activity_name, filename_name = differing_names + print(f"\n❌ Activity name and filename name are not the same in {filename}") + print(f"name: {activity_name}") + print(f"filename: {filename_name}") + + if is_created_at_present(activity) is False: + file_is_valid = False + print(f"\n❌ Missing key created_at in {filename}") + if timestamp := add_created_at(activity): + print(f"Add created_at: {timestamp}") + with open(filename, "w", encoding="utf8") as file: + yaml.dump(activity, file) + + if is_last_edited_at_present(activity) is False: + file_is_valid = False + print(f"\n❌ Missing key last_edited_at in {filename}") + if timestamp := add_last_edited_at(activity): + print(f"Add last_edited_at: {timestamp}") + with open(filename, "w", encoding="utf8") as file: + yaml.dump(activity, file) + + if is_file_modified(filename) and (timestamp := update_last_edited_at(activity)): + file_is_valid = False + print(f"\n❌ last_edited_at is not up to date in {filename}") + print(f"Update last_edited_at: {timestamp}") + with open(filename, "w", encoding="utf8") as file: + yaml.dump(activity, file) + + if missing_skills := find_missing_skills(activity["skills"]): + file_is_valid = False + print(f"\n❌ The following skills do not exist in {filename}") + for skill in missing_skills: + print(f" - {skill}") + + if missing_icons := find_missing_icons(activity, of_type="activity"): + file_is_valid = False + print(f"\n❌ The following icons do not exist in {filename}") + for icon in missing_icons: + print(f" - {icon}") + + if strings_with_newline := find_string_values_starting_with_newline(activity): + file_is_valid = False + print(f"\n❌ Found strings staring with newline in {filename}") + for string in strings_with_newline: + print(f" - {string}") + + if empty_string_value := find_empty_string_values(activity): + file_is_valid = False + print(f"\n❌ Found empty strings in {filename}") + for string in empty_string_value: + print(f" - {string}") + + return file_is_valid + + +def main(): + """Main function""" + files = get_files() + + must_fail = False + + for file in files: + file_is_valid = check_activity(file) + if file_is_valid is False: + must_fail = True + + if must_fail: + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Tools/Hooks/check_yaml_content_activities_unique_uuid.py b/Tools/Hooks/check_yaml_content_activities_unique_uuid.py new file mode 100755 index 0000000000..2e17739b5a --- /dev/null +++ b/Tools/Hooks/check_yaml_content_activities_unique_uuid.py @@ -0,0 +1,54 @@ +#!/usr/bin/python3 + +# Leka - LekaOS +# Copyright 2020 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +import os +import sys + +from pathlib import Path + + +DIRECTORY_PATH = "Modules/ContentKit/Resources/Content" + + +def find_duplicates(): + """Find duplicates in the activity files""" + path = Path(DIRECTORY_PATH) + activity_files = path.rglob("*.activity.yml") + + uuids_files = {} + duplicates = [] + + for file in activity_files: + uuid = os.path.basename(file).split("-")[-1].split(".")[0] + if uuid in uuids_files: + uuids_files[uuid].append(file) + else: + uuids_files[uuid] = [file] + + for uuid, files in uuids_files.items(): + if len(files) > 1: + duplicates.append((uuid, files)) + + return duplicates + + +def main(): + """Main function""" + duplicates = find_duplicates() + + if duplicates: + print("❌ Duplicates found:") + for uuid, files in duplicates: + print(f" - {uuid}") + for file in files: + print(f" - {file}") + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Tools/Hooks/check_yaml_definitions_authors.py b/Tools/Hooks/check_yaml_definitions_authors.py new file mode 100755 index 0000000000..4bb2a9abb8 --- /dev/null +++ b/Tools/Hooks/check_yaml_definitions_authors.py @@ -0,0 +1,49 @@ +#!/usr/bin/python3 +"""Check authors definitions""" + +# Leka - LekaOS +# Copyright 2020 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +import sys + +from modules.utils import get_files +from modules.yaml import is_jtd_schema_compliant +from modules.definitions import is_definition_list_valid + + +JTD_SCHEMA = "Specs/jtd/authors.jtd.json" + + +def check_authors_definitions(file): + """Check authors definitions""" + file_is_valid = True + if is_jtd_schema_compliant(file, JTD_SCHEMA) is False: + file_is_valid = False + + if is_definition_list_valid(file) is False: + file_is_valid = False + + return file_is_valid + + +def main(): + """Main function""" + files = get_files() + + must_fail = False + + for file in files: + file_is_valid = check_authors_definitions(file) + + if file_is_valid is False: + must_fail = True + + if must_fail: + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Tools/Hooks/check_yaml_definitions_avatars.py b/Tools/Hooks/check_yaml_definitions_avatars.py new file mode 100755 index 0000000000..e52f669464 --- /dev/null +++ b/Tools/Hooks/check_yaml_definitions_avatars.py @@ -0,0 +1,87 @@ +#!/usr/bin/python3 +"""Check avatars definitions""" + +# Leka - LekaOS +# Copyright 2024 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +import sys +from pathlib import Path + +from modules.utils import get_files +from modules.yaml import load_yaml, is_jtd_schema_compliant +from modules.definitions import find_duplicate_ids + + +JTD_SCHEMA = "Specs/jtd/avatars.jtd.json" +AVATAR_IMAGE_DIRECTORY = "Modules/AccountKit/Resources/avatars/images" + + +def find_image(image): + """Find the image file""" + start_path = Path(AVATAR_IMAGE_DIRECTORY) + image_filename = image + ".avatars.png" + for file in start_path.rglob("*.avatars.png"): + if file.name == image_filename: + return file + return None + + +def list_image_names(data): + """List of images from the YAML file""" + images = [] + category = [item["avatars"] for item in data["categories"]] + for avatars in category: + for avatar in avatars: + images.append(avatar) + return images + + +def check_avatars_definitions(file): + """Check avatars definitions""" + file_is_valid = True + + if is_jtd_schema_compliant(file, JTD_SCHEMA) is False: + file_is_valid = False + + data = load_yaml(file) + + ids = [item["id"] for item in data["categories"]] + if duplicate_ids := find_duplicate_ids(ids): + file_is_valid = False + print(f"\n❌ There are duplicate ids in {file}") + for duplicate_id in duplicate_ids: + print(f" - {duplicate_id}") + + for name in list_image_names(data): + if "-" in name: + file_is_valid = False + print(f'\n❌ The image {name}.avatars.png include "-" instead of "_"') + + if find_image(name) is None: + file_is_valid = False + print(f"\n❌ The image {name}.avatars.png in {file} does not exist") + + return file_is_valid + + +def main(): + """Main function""" + files = get_files() + + must_fail = False + + for file in files: + file_is_valid = check_avatars_definitions(file) + + if file_is_valid is False: + must_fail = True + + if must_fail: + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Tools/Hooks/check_yaml_definitions_professions.py b/Tools/Hooks/check_yaml_definitions_professions.py new file mode 100755 index 0000000000..3b0158cfc5 --- /dev/null +++ b/Tools/Hooks/check_yaml_definitions_professions.py @@ -0,0 +1,50 @@ +#!/usr/bin/python3 +"""Check profession definitions""" + +# Leka - LekaOS +# Copyright 2020 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +import sys + +from modules.utils import get_files +from modules.yaml import is_jtd_schema_compliant +from modules.definitions import is_definition_list_valid + + +JTD_SCHEMA = "Specs/jtd/professions.jtd.json" + + +def check_profession_definitions(file): + """Check profession definitions""" + file_is_valid = True + + if is_jtd_schema_compliant(file, JTD_SCHEMA) is False: + file_is_valid = False + + if is_definition_list_valid(file) is False: + file_is_valid = False + + return file_is_valid + + +def main(): + """Main function""" + files = get_files() + + must_fail = False + + for file in files: + file_is_valid = check_profession_definitions(file) + + if file_is_valid is False: + must_fail = True + + if must_fail: + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Tools/Hooks/check_yaml_definitions_skills.py b/Tools/Hooks/check_yaml_definitions_skills.py new file mode 100755 index 0000000000..ac3fbde764 --- /dev/null +++ b/Tools/Hooks/check_yaml_definitions_skills.py @@ -0,0 +1,77 @@ +#!/usr/bin/python3 +"""Check skill definitions""" + +# Leka - LekaOS +# Copyright 2020 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +import sys + +from modules.utils import get_files +from modules.yaml import load_yaml, is_jtd_schema_compliant, dump_yaml +from modules.definitions import find_duplicate_ids, sort_list_by_id + + +JTD_SCHEMA = "Specs/jtd/skills.jtd.json" +SKILLS_FILE = "Modules/ContentKit/Resources/Content/definitions/skills.yml" + + +def get_all_skills(): + """List of skills from skills.yml""" + skills = load_yaml(SKILLS_FILE) + + ids = [] + + def find_skill_ids(data, ids): + for item in data: + ids.append(item["id"]) + if "subskills" in item and item["subskills"]: + find_skill_ids(item["subskills"], ids) + + find_skill_ids(skills["list"], ids) + + return ids + + +def check_skills_definitions(file): + """Check skills definitions""" + file_is_valid = True + + if is_jtd_schema_compliant(file, JTD_SCHEMA) is False: + file_is_valid = False + + data = load_yaml(file) + + if sorted_list := sort_list_by_id(data["list"]): + data["list"] = sorted_list + dump_yaml(file, data) + + if duplicate_ids := find_duplicate_ids(get_all_skills()): + file_is_valid = False + print(f"\n❌ There are duplicate ids in {file}") + for duplicate_id in duplicate_ids: + print(f" - {duplicate_id}") + + return file_is_valid + + +def main(): + """Main function""" + files = get_files() + + must_fail = False + + for file in files: + file_is_valid = check_skills_definitions(file) + + if file_is_valid is False: + must_fail = True + + if must_fail: + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Tools/Hooks/modules/content.py b/Tools/Hooks/modules/content.py new file mode 100755 index 0000000000..6bc2bb5932 --- /dev/null +++ b/Tools/Hooks/modules/content.py @@ -0,0 +1,192 @@ +#!/usr/bin/python3 +"""Check activities and curriculums content""" + +# Leka - LekaOS +# Copyright 2020 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +import os +import uuid +from pathlib import Path +from datetime import datetime, timedelta + +import ruamel.yaml + +from check_yaml_definitions_skills import get_all_skills + +DATE_NOW_TIMESTAMP = ruamel.yaml.scalarstring.DoubleQuotedScalarString( + datetime.now().isoformat() +) + +CONTENTKIT_DIRECTORY = "Modules/ContentKit/Resources/Content" + +CREATED_AT_INDEX = 3 +LAST_EDITED_AT_INDEX = 4 + + +def is_uuid_same_as_filename(activity, filename): + """Check if the UUID is the same as the filename""" + activity_uuid = activity["uuid"] + filename_uuid = os.path.basename(filename).split("-")[-1].split(".")[0] + + if activity_uuid != filename_uuid: + return (activity_uuid, filename_uuid) + + return None + + +def is_uuid_valid(uuid_to_check): + """Check if the UUID is valid""" + try: + uuid.UUID(uuid_to_check) + return True + except ValueError: + return False + + +def is_name_same_as_filename(activity, filename): + """Check if the name is the same as the filename""" + activity_name = activity["name"] + filename_name = os.path.basename(filename).split("-")[0] + + if activity_name != filename_name: + return (activity_name, filename_name) + + return None + + +def is_created_at_present(data): + """Check if the created_at field is present""" + return "created_at" in data + + +def add_created_at(data): + """Add the created_at field to the YAML file""" + if "name" in data and "status" in data: + data.insert(CREATED_AT_INDEX, "created_at", DATE_NOW_TIMESTAMP) + return DATE_NOW_TIMESTAMP + + return None + + +def is_last_edited_at_present(data): + """Check if the last_edited_at field is present""" + return "last_edited_at" in data + + +def add_last_edited_at(data): + """Add the last_edited_at field to the YAML file""" + if "name" in data and "status" in data: + data.insert(LAST_EDITED_AT_INDEX, "last_edited_at", DATE_NOW_TIMESTAMP) + return DATE_NOW_TIMESTAMP + + return None + + +def update_last_edited_at(data): + """Update the last_edited_at field to the YAML file""" + last_edited_at = datetime.fromisoformat(data["last_edited_at"]) + now_minus_delta = datetime.fromisoformat(DATE_NOW_TIMESTAMP) - timedelta(minutes=1) + + if last_edited_at < now_minus_delta: + data["last_edited_at"] = DATE_NOW_TIMESTAMP + return DATE_NOW_TIMESTAMP + + return None + + +def find_missing_skills(skills): + """Check if the skills exist in the skills.yml""" + skills_ids = get_all_skills() + + missing_skills = [] + + for skill in skills: + if skill not in skills_ids: + missing_skills.append(skill) + + return missing_skills + + +def find_icon(icon): + """Find the icon file""" + start_path = Path(CONTENTKIT_DIRECTORY) + icon_filename = icon + ".icon.png" + for file in start_path.rglob("*.icon.png"): + if file.name == icon_filename: + return file + return None + + +def list_icons(data): + """List of icons from the YAML file""" + icons = [] + for l10n_entry in data["l10n"]: + if "details" in l10n_entry and "icon" in l10n_entry["details"]: + icons.append(l10n_entry["details"]["icon"]) + return icons + + +def find_missing_icons(data: str, of_type: str): + """Check if the icon exists""" + icons = list_icons(data) + + missing_icons = [] + + for icon in icons: + if of_type == "activity": + icon_name = icon + ".activity" + if of_type == "curriculum": + icon_name = icon + ".curriculum" + if find_icon(icon_name) is None: + missing_icons.append(icon) + + return missing_icons + + +def find_string_values_starting_with_newline(data, path=None): + """Check if a string starts with a newline character""" + if path is None: + path = [] # Initialize path + + keys_with_newlines = [] + + if isinstance(data, dict): # If the item is a dictionary + for key, value in data.items(): + keys_with_newlines += find_string_values_starting_with_newline( + value, path + [key] + ) + elif isinstance(data, list): # If the item is a list + for index, item in enumerate(data): + keys_with_newlines += find_string_values_starting_with_newline( + item, path + [index] + ) + elif isinstance(data, str): # If the item is a string + if data.startswith("\n"): + keys_with_newlines.append("/".join(map(str, path))) + + return keys_with_newlines + + +def find_empty_string_values(data, path=None): + """Check for empty strings in the data structure""" + if path is None: + path = [] + + keys_with_empty_strings = [] + + if isinstance(data, dict): + for key, value in data.items(): + sub_path = path + [key] # Build the path for nested dictionaries + if isinstance(value, str) and (value == "" or value.isspace()): + keys_with_empty_strings.append("/".join(map(str, sub_path))) + else: + # Recurse into the value if it's a dict or list + keys_with_empty_strings += find_empty_string_values(value, sub_path) + elif isinstance(data, list): + for index, item in enumerate(data): + sub_path = path + [str(index)] # Handle list indexing in the path + # Recurse into the item if it's a dict or list + keys_with_empty_strings += find_empty_string_values(item, sub_path) + + return keys_with_empty_strings diff --git a/Tools/Hooks/modules/definitions.py b/Tools/Hooks/modules/definitions.py new file mode 100755 index 0000000000..a54064d06f --- /dev/null +++ b/Tools/Hooks/modules/definitions.py @@ -0,0 +1,49 @@ +#!/usr/bin/python3 +"""Utily functions for the hooks.""" + +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +from modules.yaml import load_yaml, dump_yaml + + +def sort_list_by_id(data): + """Sort a list of dictionaries by id.""" + return sorted(data, key=lambda item: item["id"]) + + +def find_duplicate_ids(ids): + """Check if all ids are unique.""" + duplicates = set() + + if len(set(ids)) != len(ids): + seen = set() + + for author_id in ids: + if author_id in seen: + duplicates.add(author_id) + else: + seen.add(author_id) + + return duplicates + + +def is_definition_list_valid(file): + """Check definitions""" + file_is_valid = True + + data = load_yaml(file) + + if sorted_list := sort_list_by_id(data["list"]): + data["list"] = sorted_list + dump_yaml(file, data) + + ids = [item["id"] for item in data["list"]] + if duplicate_ids := find_duplicate_ids(ids): + file_is_valid = False + print(f"\n❌ There are duplicate ids in {file}") + for duplicate_id in duplicate_ids: + print(f" - {duplicate_id}") + + return file_is_valid diff --git a/Tools/Hooks/modules/utils.py b/Tools/Hooks/modules/utils.py new file mode 100755 index 0000000000..9b838e02ab --- /dev/null +++ b/Tools/Hooks/modules/utils.py @@ -0,0 +1,42 @@ +#!/usr/bin/python3 +"""Utily functions for the hooks.""" + +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +import sys +import subprocess + + +def get_files(): + """Get the files from the command line arguments.""" + if len(sys.argv) <= 1: + print("\n❌ No file specified") + sys.exit(1) + + files = sys.argv[1:] + return files + + +def is_file_modified(file_path): + """Check if a file is modified and/or staged for commit""" + result = subprocess.run( + ["git", "status", "--porcelain", file_path], + capture_output=True, + text=True, + check=False, + ) + + output = result.stdout.strip() + + if output: + # Check for modifications in both staged (index) and work tree + # Staged modifications: first letter is not ' ' (space) + # Work tree modifications: second letter is 'M' + # This covers added (A), modified (M), deleted (D), renamed (R), etc. + status_code = output[:2] + if status_code[0] != " " or status_code[1] == "M": + return True + + return False diff --git a/Tools/Hooks/modules/xcstrings.py b/Tools/Hooks/modules/xcstrings.py new file mode 100755 index 0000000000..877b1457ed --- /dev/null +++ b/Tools/Hooks/modules/xcstrings.py @@ -0,0 +1,44 @@ +#!/usr/bin/python3 +"""Module providing a hook to check for stale entries in .xcstrings files.""" + +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +import json + + +def find_stale_entries(json_file): + """Check for stale entries in a .xcstrings file.""" + with open(json_file, "r", encoding="utf8") as file: + data = json.load(file) + strings = data.get("strings", {}) + + stale_entries = [] + + for key, value in strings.items(): + if value.get("extractionState") == "stale": + stale_entries.append((key, strings[key])) + + return stale_entries + + +def find_unusual_characters(file_path): + """Check the given file for unusual terminators.""" + with open(file_path, "r", encoding="utf-8") as file: + data = json.load(file) + + characters_to_search = [ + "\u2028", + ] + + wrong_entries = [] + + for key, value in data["strings"].items(): + for _, localizations in value["localizations"].items(): + localized_string = localizations["stringUnit"]["value"] + for character in characters_to_search: + if character in localized_string: + wrong_entries.append((key, value, character)) + + return wrong_entries diff --git a/Tools/Hooks/modules/yaml.py b/Tools/Hooks/modules/yaml.py new file mode 100755 index 0000000000..23da5ec515 --- /dev/null +++ b/Tools/Hooks/modules/yaml.py @@ -0,0 +1,59 @@ +#!/usr/bin/python3 +"""Utily functions for the hooks.""" + +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +import os +import subprocess + +import ruamel.yaml + + +def create_yaml_object(): + """Create a YAML object""" + yaml = ruamel.yaml.YAML(typ="rt") + yaml.indent(mapping=2, sequence=4, offset=2) + yaml.preserve_quotes = True + yaml.representer.add_representer( + type(None), + lambda dumper, data: dumper.represent_scalar("tag:yaml.org,2002:null", "null"), + ) + return yaml + + +def load_yaml(filename): + """Load a YAML file.""" + yaml = create_yaml_object() + + with open(filename, "r", encoding="utf8") as file: + data = yaml.load(file) + + return data + + +def dump_yaml(filename, data): + """Dump a YAML file.""" + yaml = create_yaml_object() + + with open(filename, "w", encoding="utf8") as file: + yaml.dump(data, file) + + +def is_jtd_schema_compliant(filename, schema): + """Validate a YAML file with a JTD schema.""" + file_is_compliant = True + + os.environ["FORCE_COLOR"] = "true" + cmd = f"ajv validate --verbose --all-errors --spec=jtd -s {schema} -d {filename}" + + result = subprocess.run(cmd, shell=True, capture_output=True, check=False) + + if result.returncode != 0: + error = result.stderr.decode("utf-8") + print(f"\n❌ File does not match the schema {schema}") + print(error) + file_is_compliant = False + + return file_is_compliant diff --git a/Tools/Scripts/git_log_to_notes.py b/Tools/Scripts/git_log_to_notes.py new file mode 100755 index 0000000000..d8b4c6dfc9 --- /dev/null +++ b/Tools/Scripts/git_log_to_notes.py @@ -0,0 +1,100 @@ +#!/usr/bin/python3 +"""Get clean git log to be used for TestFlight notes and Slack""" + +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +import argparse +import re +import subprocess +import sys + + +def parse_arguments(): + """Parse command line arguments for release note generation.""" + parser = argparse.ArgumentParser( + prog="Git log to release notes", + description="Get clean git log to be used for TestFlight notes and Slack", + ) + + parser.add_argument( + "-r", + "--remove-emojis", + action="store_true", + default=False, + help="Remove emojis from the git log", + ) + + parser.add_argument( + "-f", "--first-commit", help="First commit of the release", required=True + ) + + parser.add_argument( + "-l", "--last-commit", help="Last commit of the release", required=True + ) + + return parser.parse_args() + + +def remove_emojis(text): + """Remove emojis and other non-ASCII characters from text.""" + emoji_pattern = re.compile( + "[" + "\U0001F600-\U0001F64F" # emoticons + "\U0001F300-\U0001F5FF" # symbols & pictographs + "\U0001F680-\U0001F6FF" # transport & map symbols + "\U0001F700-\U0001F77F" # alchemical symbols + "\U0001F780-\U0001F7FF" # Geometric Shapes Extended + "\U0001F800-\U0001F8FF" # Supplemental Arrows-C + "\U0001F900-\U0001F9FF" # Supplemental Symbols and Pictographs + "\U0001FA00-\U0001FA6F" # Chess Symbols + "\U0001FA70-\U0001FAFF" # Symbols and Pictographs Extended-A + "\U00002702-\U000027B0" # Dingbats + "\U000024C2-\U0001F251" + "]+", + flags=re.UNICODE, + ) + return re.sub(emoji_pattern, "", text) + + +def git_log(first_commit, last_commit, remove_emojis_flag): + """Get the git log between two commits, excluding merge commits, and remove emojis.""" + cmd = [ + "git", + "log", + "--reverse", + "--oneline", + "--no-merges", + "--format= -%s" if remove_emojis_flag else "--format= - %s", + f"{first_commit}..{last_commit}", + ] + + try: + result = subprocess.run(cmd, capture_output=True, check=True, text=True) + output = remove_emojis(result.stdout) if remove_emojis_flag else result.stdout + return True, output + except subprocess.CalledProcessError as e: + error_message = ( + f"Could not get the git log between {first_commit} and {last_commit}:\n" + f"{e.stderr}" + ) + return False, error_message + + +def main(): + """Main function to parse arguments and generate git log""" + args = parse_arguments() + + success, message = git_log(args.first_commit, args.last_commit, args.remove_emojis) + + if not success: + print(message, file=sys.stderr) + return 1 + + print(message) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Tuist/.swiftformat b/Tuist/.swiftformat new file mode 100644 index 0000000000..23dddd49f3 --- /dev/null +++ b/Tuist/.swiftformat @@ -0,0 +1,6 @@ +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +--exclude Dependencies, Signing +--disable acronyms diff --git a/Tuist/Config.swift b/Tuist/Config.swift index 087cbf3dc2..faaa629ab3 100644 --- a/Tuist/Config.swift +++ b/Tuist/Config.swift @@ -1,3 +1,9 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + import ProjectDescription let config = Config( diff --git a/Tuist/Dependencies.swift b/Tuist/Dependencies.swift new file mode 100644 index 0000000000..92eca18d4c --- /dev/null +++ b/Tuist/Dependencies.swift @@ -0,0 +1,12 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription + +let dependencies = Dependencies( + swiftPackageManager: .init(), + platforms: [.iOS, .macOS] +) diff --git a/Tuist/Package.resolved b/Tuist/Package.resolved new file mode 100644 index 0000000000..c9f9c7aea4 --- /dev/null +++ b/Tuist/Package.resolved @@ -0,0 +1,221 @@ +{ + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bfc0b6f81adc06ce5121eb23f628473638d67c5c", + "version" : "1.2022062300.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "3e464dad87dad2d29bb29a97836789bf0f8f67d2", + "version" : "10.18.1" + } + }, + { + "identity" : "audiokit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/AudioKit/AudioKit", + "state" : { + "revision" : "82cab9d4c168b5b03b67e2ff099819f589b530f7", + "version" : "5.6.2" + } + }, + { + "identity" : "combinecorebluetooth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/StarryInternet/CombineCoreBluetooth", + "state" : { + "revision" : "eaadfdbec3b553fec5b40c27d8d40847adb23d40", + "version" : "0.7.2" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "b880ec8ec927a838c51c12862c6222c30d7097d7", + "version" : "10.20.0" + } + }, + { + "identity" : "fit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/OlehKorchytskyi/Fit", + "state" : { + "revision" : "4532ba51e141c750bd52c73556043fdd8ca9e8d0", + "version" : "1.0.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "ceec9f28dea12b7cf3dabf18b5ed7621c88fd4aa", + "version" : "10.20.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "a732a4b47f59e4f725a2ea10f0c77e93a7131117", + "version" : "9.3.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "bc27fad73504f3d4af235de451f02ee22586ebd3", + "version" : "7.12.1" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "a673bc2937fbe886dd1f99c401b01b6d977a9c98", + "version" : "1.49.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "115f75e43851774934d695449a4836123c3246e1", + "version" : "3.2.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648", + "version" : "100.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "9d108e9112aa1d65ce508facf804674546116d9c", + "version" : "1.22.3" + } + }, + { + "identity" : "lottie-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/airbnb/lottie-ios", + "state" : { + "revision" : "7fe8b6f697ae7db4bf0df270119592cb5d502848", + "version" : "4.4.1" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692", + "version" : "2.30909.0" + } + }, + { + "identity" : "networkimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/NetworkImage", + "state" : { + "revision" : "7aff8d1b31148d32c5933d75557d42f6323ee3d1", + "version" : "6.0.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e", + "version" : "2.3.1" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log", + "state" : { + "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", + "version" : "1.5.4" + } + }, + { + "identity" : "swift-markdown-ui", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/swift-markdown-ui", + "state" : { + "revision" : "ae799d015a5374708f7b4c85f3294c05f2a564e2", + "version" : "2.3.0" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "65e8f29b2d63c4e38e736b25c27b83e012159be8", + "version" : "1.25.2" + } + }, + { + "identity" : "swiftuijoystick", + "kind" : "remoteSourceControl", + "location" : "https://github.com/michael94ellis/SwiftUIJoystick", + "state" : { + "revision" : "5bd303cdafb369a70a45c902538b42dd3c5f4d65", + "version" : "1.5.0" + } + }, + { + "identity" : "version", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mxcl/Version", + "state" : { + "revision" : "1fe824b80d89201652e7eca7c9252269a1d85e25", + "version" : "2.0.1" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams", + "state" : { + "revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3", + "version" : "5.0.6" + } + } + ], + "version" : 2 +} diff --git a/Tuist/Package.swift b/Tuist/Package.swift new file mode 100644 index 0000000000..1f234f3d11 --- /dev/null +++ b/Tuist/Package.swift @@ -0,0 +1,58 @@ +// swift-tools-version: 5.9 +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import PackageDescription + +let package = Package( + name: "GlobalProjectDependencies", + dependencies: [ + .package( + url: "https://github.com/jpsim/Yams", + exact: "5.0.6" + ), + .package( + url: "https://github.com/airbnb/lottie-ios", + exact: "4.4.1" + ), + .package( + url: "https://github.com/gonzalezreal/swift-markdown-ui", + exact: "2.3.0" + ), + .package( + url: "https://github.com/apple/swift-argument-parser", + exact: "1.3.0" + ), + .package( + url: "https://github.com/StarryInternet/CombineCoreBluetooth", + exact: "0.7.2" + ), + .package( + url: "https://github.com/michael94ellis/SwiftUIJoystick", + exact: "1.5.0" + ), + .package( + url: "https://github.com/AudioKit/AudioKit", + exact: "5.6.2" + ), + .package( + url: "https://github.com/apple/swift-log", + exact: "1.5.4" + ), + .package( + url: "https://github.com/mxcl/Version", + exact: "2.0.1" + ), + .package( + url: "https://github.com/firebase/firebase-ios-sdk", + exact: "10.20.0" + ), + .package( + url: "https://github.com/OlehKorchytskyi/Fit", + exact: "1.0.0" + ), + ] +) diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist+Base.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist+Base.swift new file mode 100644 index 0000000000..9db5735f7a --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/InfoPlist+Base.swift @@ -0,0 +1,43 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription + +public extension InfoPlist { + static func base(version: String) -> [String: Plist.Value] { + [ + "CFBundleShortVersionString": "\(version)", + "CFBundleVersion": "\(version)", + "UIMainStoryboardFile": "", + "UILaunchStoryboardName": "LaunchScreen", + "ITSAppUsesNonExemptEncryption": "NO", + "CFBundleLocalizations": [ + "fr", + "en", + ], + "NSBluetoothAlwaysUsageDescription": + "The app needs to use Bluetooth to connect to the Leka robot", + "UIBackgroundModes": [ + "bluetooth-central", + ], + "UIRequiresFullScreen": "true", + "UISupportedInterfaceOrientations": [ + "UIInterfaceOrientationLandscapeRight", + "UIInterfaceOrientationLandscapeLeft", + ], + "UISupportedInterfaceOrientations~ipad": [ + "UIInterfaceOrientationLandscapeRight", + "UIInterfaceOrientationLandscapeLeft", + ], + "LSApplicationCategoryType": "public.app-category.utilities", + "LSMinimumSystemVersion": "14.0", + ] + } + + static func extendingBase(version: String, with plist: [String: Plist.Value]) -> [String: Plist.Value] { + self.base(version: version).merging(plist) { _, new in new } + } +} diff --git a/Tuist/ProjectDescriptionHelpers/Project+App.swift b/Tuist/ProjectDescriptionHelpers/Project+App.swift new file mode 100644 index 0000000000..13a02af575 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Project+App.swift @@ -0,0 +1,63 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription + +public extension Project { + static func app( + name: String, + version: String = "1.0.0", + deploymentTargets: DeploymentTargets = .iOS("16.6"), + destinations: Destinations = [.iPad, .macWithiPadDesign], + infoPlist: [String: Plist.Value] = [:], + settings: SettingsDictionary = [:], + options: Options = .options(), + dependencies: [TargetDependency] = [], + schemes: [Scheme] = [] + ) -> Project { + let mainTarget = Target( + name: name, + destinations: destinations, + product: .app, + bundleId: "io.leka.apf.app.\(name)", + deploymentTargets: deploymentTargets, + infoPlist: .extendingDefault(with: InfoPlist.extendingBase(version: version, with: infoPlist)), + sources: ["Sources/**"], + resources: ["Resources/**"], + scripts: TargetScript.linters(), + dependencies: dependencies, + settings: .settings(base: .extendingBase(with: settings)), + environmentVariables: [ + "IDEPreferLogStreaming": "YES", + ] + ) + + let testTarget = Target( + name: "\(name)Tests", + destinations: destinations, + product: .unitTests, + bundleId: "io.leka.apf.app.\(name)Tests", + infoPlist: .extendingDefault(with: InfoPlist.extendingBase(version: version, with: infoPlist)), + sources: ["Tests/**"], + resources: [], + scripts: TargetScript.linters(), + dependencies: [ + .target(name: "\(name)"), + ] + ) + + return Project( + name: name, + organizationName: "leka.io", + options: options, + targets: [ + mainTarget, + testTarget, + ], + schemes: l10nSchemes(name: name) + schemes + ) + } +} diff --git a/Tuist/ProjectDescriptionHelpers/Project+Cli.swift b/Tuist/ProjectDescriptionHelpers/Project+Cli.swift new file mode 100644 index 0000000000..e130433dfd --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Project+Cli.swift @@ -0,0 +1,34 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription + +public extension Project { + static func cli( + name: String, + version _: String = "1.0.0", + dependencies: [TargetDependency] + ) -> Project { + let mainTarget = Target( + name: name, + destinations: .macOS, + product: .commandLineTool, + bundleId: "io.leka.apf.cli.\(name)", + deploymentTargets: .macOS("13.0"), + sources: ["Sources/**"], + scripts: TargetScript.linters(), + dependencies: dependencies + ) + + let targets = [mainTarget] + + return Project( + name: name, + organizationName: "leka.io", + targets: targets + ) + } +} diff --git a/Tuist/ProjectDescriptionHelpers/Project+Module.swift b/Tuist/ProjectDescriptionHelpers/Project+Module.swift new file mode 100644 index 0000000000..cdef124614 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Project+Module.swift @@ -0,0 +1,119 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription + +// MARK: - ModuleExample + +public struct ModuleExample { + // MARK: Lifecycle + + public init(name: String, infoPlist: [String: Plist.Value] = [:], dependencies: [TargetDependency] = []) { + self.name = name + self.infoPlist = infoPlist + self.dependencies = dependencies + } + + // MARK: Public + + public let name: String + public let infoPlist: [String: Plist.Value] + public let dependencies: [TargetDependency] +} + +public extension Project { + static func module( + name: String, + deploymentTargets: DeploymentTargets = .iOS("16.0"), + destinations: Destinations = [.iPad, .macWithiPadDesign], + infoPlist: [String: Plist.Value] = [:], + settings: SettingsDictionary = [:], + options: Options = .options(), + examples: [ModuleExample] = [], + dependencies: [TargetDependency] = [], + schemes: [Scheme] = [] + ) -> Project { + let frameworkTargets = makeFrameworkTargets( + name: name, + deploymentTargets: deploymentTargets, + destinations: destinations, + infoPlist: infoPlist, + settings: settings, + dependencies: dependencies + ) + + let exampleTargets = examples.compactMap { example in + Target( + name: example.name, + destinations: destinations, + product: .app, + bundleId: "io.leka.apf.app.example.\(example.name)", + deploymentTargets: deploymentTargets, + infoPlist: .extendingDefault(with: InfoPlist.extendingBase(version: "1.0.0", with: infoPlist)), + sources: ["Examples/\(example.name)/Sources/**"], + resources: ["Examples/\(example.name)/Resources/**"], + scripts: TargetScript.linters(), + dependencies: [.target(name: name)] + example.dependencies, + settings: .settings(base: .extendingBase(with: settings)) + ) + } + + return Project( + name: name, + organizationName: "leka.io", + options: options, + targets: frameworkTargets + exampleTargets, + schemes: schemes + ) + } +} + +private func makeFrameworkTargets( + name: String, + deploymentTargets: DeploymentTargets = .iOS("16.0"), + destinations: Destinations = [.iPad, .macWithiPadDesign], + infoPlist: [String: Plist.Value] = [:], + settings: SettingsDictionary = [:], + dependencies: [TargetDependency] = [] +) + -> [Target] +{ + var product: Product = .staticLibrary + + if Environment.generateModulesAsFrameworksForDebug.getBoolean(default: false) { + product = .framework + } + + let module = Target( + name: name, + destinations: destinations, + product: product, + bundleId: "io.leka.apf.module.\(name)", + deploymentTargets: deploymentTargets, + infoPlist: .extendingDefault(with: InfoPlist.extendingBase(version: "1.0.0", with: infoPlist)), + sources: ["Sources/**"], + resources: ["Resources/**"], + scripts: TargetScript.linters(), + dependencies: dependencies, + settings: .settings(base: .extendingBase(with: settings)) + ) + + let tests = Target( + name: "\(name)Tests", + destinations: destinations, + product: .unitTests, + bundleId: "io.leka.apf.framework.\(name)Tests", + infoPlist: .extendingDefault(with: InfoPlist.extendingBase(version: "1.0.0", with: infoPlist)), + sources: ["Tests/**"], + resources: [], + scripts: TargetScript.linters(), + dependencies: [ + .target(name: name), + ] + ) + + return [module, tests] +} diff --git a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift b/Tuist/ProjectDescriptionHelpers/Project+Templates.swift deleted file mode 100644 index ccf986ab09..0000000000 --- a/Tuist/ProjectDescriptionHelpers/Project+Templates.swift +++ /dev/null @@ -1,90 +0,0 @@ -import ProjectDescription - -/// Project helpers are functions that simplify the way you define your project. -/// Share code to create targets, settings, dependencies, -/// Create your own conventions, e.g: a func that makes sure all shared targets are "static frameworks" -/// See https://docs.tuist.io/guides/helpers/ - -extension Project { - /// Helper function to create the Project for this ExampleApp - public static func app(name: String, platform: Platform, dependencies: [TargetDependency]) -> Project { - let targets = makeAppTargets(name: name, - platform: platform, - dependencies: dependencies) - return Project(name: name, - organizationName: "leka.io", - targets: targets) - } - - /// Helper function to create the Project for this ExampleApp - public static func module(name: String, platform: Platform, dependencies: [TargetDependency]) -> Project { - let targets = makeFrameworkTargets(name: name, - platform: platform, - dependencies: dependencies) - - return Project(name: name, - organizationName: "leka.io", - targets: targets) - } - - // - // MARK: - Private - // - - /// Helper function to create a framework target and an associated unit test target - private static func makeFrameworkTargets(name: String, platform: Platform, dependencies: [TargetDependency]) -> [Target] { - let sources = Target(name: name, - platform: platform, - product: .staticLibrary, - bundleId: "io.leka.apf.framework.\(name)", - infoPlist: .default, - sources: ["Sources/**"], - resources: ["Resources/**"], - dependencies: dependencies) - - let tests = Target(name: "\(name)Tests", - platform: platform, - product: .unitTests, - bundleId: "io.leka.apf.framework.\(name)Tests", - infoPlist: .default, - sources: ["Tests/**"], - resources: [], - dependencies: [.target(name: name)]) - - return [sources, tests] - } - - /// Helper function to create the application target and the unit test target. - private static func makeAppTargets(name: String, platform: Platform, dependencies: [TargetDependency]) -> [Target] { - let platform: Platform = platform - let infoPlist: [String: InfoPlist.Value] = [ - "CFBundleShortVersionString": "1.0", - "CFBundleVersion": "1", - "UIMainStoryboardFile": "", - "UILaunchStoryboardName": "LaunchScreen" - ] - - let mainTarget = Target( - name: name, - platform: platform, - product: .app, - bundleId: "io.leka.apf.app.\(name)", - infoPlist: .extendingDefault(with: infoPlist), - sources: ["Sources/**"], - resources: ["Resources/**"], - dependencies: dependencies - ) - - let testTarget = Target( - name: "\(name)Tests", - platform: platform, - product: .unitTests, - bundleId: "io.leka.apf.app.\(name)Tests", - infoPlist: .default, - sources: ["Tests/**"], - dependencies: [ - .target(name: "\(name)") - ]) - return [mainTarget, testTarget] - } -} diff --git a/Tuist/ProjectDescriptionHelpers/Scheme+l10n.swift b/Tuist/ProjectDescriptionHelpers/Scheme+l10n.swift new file mode 100644 index 0000000000..58d50d1766 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Scheme+l10n.swift @@ -0,0 +1,34 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription + +public extension Scheme { + static func l10nFR(name: String) -> Scheme { + Scheme( + name: "\(name) 🇫🇷", + shared: true, + buildAction: BuildAction(targets: ["\(name)"]), + runAction: RunAction.runAction(configuration: "Debug", options: .options(language: "fr")) + ) + } + + static func l10nEN(name: String) -> Scheme { + Scheme( + name: "\(name) 🇺🇸", + shared: true, + buildAction: BuildAction(targets: ["\(name)"]), + runAction: RunAction.runAction(configuration: "Debug", options: .options(language: "en")) + ) + } +} + +func l10nSchemes(name: String) -> [Scheme] { + [ + .l10nFR(name: name), + .l10nEN(name: name), + ] +} diff --git a/Tuist/ProjectDescriptionHelpers/SettingsDictionary+Base.swift b/Tuist/ProjectDescriptionHelpers/SettingsDictionary+Base.swift new file mode 100644 index 0000000000..d5dbd47f33 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/SettingsDictionary+Base.swift @@ -0,0 +1,26 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription + +extension SettingsDictionary { + static var base: SettingsDictionary = [ + "LOCALIZED_STRING_MACRO_NAMES": [ + "NSLocalizedString", + "CFCopyLocalizedString", + "LocalizedString", + "LocalizedStringInterpolation", + ], + "LOCALIZED_STRING_SWIFTUI_SUPPORT": "NO", + "OTHER_LDFLAGS": [ + "-ObjC", + ], + ] + + static func extendingBase(with settings: SettingsDictionary) -> SettingsDictionary { + self.base.merging(settings) { _, new in new } + } +} diff --git a/Tuist/ProjectDescriptionHelpers/TargetScripts.swift b/Tuist/ProjectDescriptionHelpers/TargetScripts.swift new file mode 100644 index 0000000000..761ae2f641 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/TargetScripts.swift @@ -0,0 +1,29 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + +import ProjectDescription + +public extension TargetScript { + static let swiftLint = TargetScript.post( + path: Path.relativeToRoot("Scripts/SwiftLintRunScript.sh"), + name: "SwiftLint", + basedOnDependencyAnalysis: false + ) + + static func linters() -> [TargetScript] { + let turnOffLinters = Environment.turnOffLinters.getBoolean(default: false) + + let defaultLinters: [TargetScript] = [ + .swiftLint, + ] + + if turnOffLinters { + return [] + } + + return defaultLinters + } +} diff --git a/Tuist/master.key b/Tuist/master.key new file mode 100644 index 0000000000..c1450c4a5b --- /dev/null +++ b/Tuist/master.key @@ -0,0 +1 @@ +5JakoSmsJv/v5Z0LJ0JgihCjfVB3Q+fnrECqvFXsU2E= diff --git a/Workspace.swift b/Workspace.swift index d48f20f84d..b763e4fc89 100644 --- a/Workspace.swift +++ b/Workspace.swift @@ -1,14 +1,66 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +// swiftformat:disable acronyms + import ProjectDescription +var projects: [Path] { + // MARK: - iOS Apps + + let iOSApps: [Path] = [ + "Apps/LekaApp", + "Apps/LekaUpdater", + // "Apps/LekaActivityUIExplorer", + ] + + // MARK: - macOS Apps + + let macOSApps: [Path] = [ + // no apps yet + ] + + // MARK: - Modules + + let modules: [Path] = [ + "Modules/AccountKit", + "Modules/BLEKit", + "Modules/ContentKit", + "Modules/DesignKit", + "Modules/GameEngineKit", + "Modules/LocalizationKit", + "Modules/LogKit", + "Modules/RobotKit", + ] + + // MARK: - iOS Examples + + let iOSExamples: [Path] = [ + "Examples/iOSApp", + "Examples/Module", + ] + + // MARK: - macOS Examples + + let macOSExamples: [Path] = [ + "Examples/macOSApp", + "Examples/macOSCli", + "Examples/Module", + ] + + var projects = iOSApps + modules + iOSExamples + + let generateMacOSApps = Environment.generateMacOSApps.getBoolean(default: false) + + if generateMacOSApps { + projects = macOSApps + modules + macOSExamples + } + + return projects +} + let workspace = Workspace( - name: "ios-monorepo", - projects: [ - // MARK: - Apps - "Apps/LekaApp", - "Apps/LekaEmotions", - "Apps/LekaUpdater", - - // MARK: - Modules - "Modules/CoreUI", - ] + name: "ios-monorepo", + projects: projects ) diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 0000000000..4df50956e9 --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +apple_id('apple.apf@leka.io') # Apple Developer Portal username +team_id('GKQJXACKX7') # Developer Portal Team ID +itc_team_id('124055768') # App Store Connect Team ID diff --git a/fastlane/Deliverfile b/fastlane/Deliverfile new file mode 100644 index 0000000000..dd469fd9bb --- /dev/null +++ b/fastlane/Deliverfile @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +# If you want to have even more control, check out the documentation +# https://docs.fastlane.tools/actions/deliver/ + +username('apple.apf@leka.io') # your Apple ID user + +submit_for_review(false) +automatic_release(false) + +languages(%w[en-US fr-FR]) diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000000..a2bffc5845 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,493 @@ +# frozen_string_literal: true + +# Leka - iOS Monorepo +# Copyright APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +# auto update fastlane +# update_fastlane + +fastlane_require 'json' +fastlane_require 'open3' + +default_platform(:ios) + +ROOT_DIR = File.expand_path('..', Dir.pwd).to_s.freeze +SIGNING_DIR = "#{ROOT_DIR}/Tuist/Signing" + +RELEASE_BUNDLE_IDS = [ + 'io.leka.apf.app.LekaApp', + 'io.leka.apf.app.LekaUpdater', + 'io.leka.apf.app.LekaActivityUIExplorer' +].freeze + +DEVELOPMENT_WILDCARD_BUNDLE_ID = 'io.leka.apf.*' + +platform :ios do + desc "Placeholder lane to make sure fastlane's working" + lane :helloworld do + UI.important "Hello, Fastlane! (from #{Dir.pwd}, root: #{ROOT_DIR}, signing: #{SIGNING_DIR})" + end + + desc 'Copy certificates for tuist' + lane :copy_certificates do |options| + UI.important 'Delete previous provisioning profiles' + system('rm', '-rf', "#{SIGNING_DIR}/{*.mobileprovision,*.p12*,*.cer*}") + + UI.important 'Create development provisioning profile' + UI.message 'Generate json graph' + + Dir.chdir('..') do + system('tuist', 'generate', '-n') + system( + 'tuist graph ' \ + '--format json ' \ + '--skip-test-targets --skip-external-dependencies ' \ + "--output-path #{SIGNING_DIR}/raw" + ) + end + + UI.message 'Parse json graph' + json = File.read("#{SIGNING_DIR}/raw/graph.json") + graph = JSON.parse(json) + all_apps = graph['projects'].values.flat_map do |project| + project['targets'].filter_map do |target| + target['name'] if target['product'] == 'app' || target['product'] == 'unit_tests' + end + end + + UI.message 'Copy wildcard provisioning profile for each app' + all_apps.each do |app| + UI.message "App: #{app} --> Bundle ID: io.leka.apf.*" + system( + 'cp -f ' \ + "#{SIGNING_DIR}/raw/dev/Development_io.leka.apf.*.mobileprovision " \ + "#{SIGNING_DIR}/#{app}.Debug.mobileprovision" + ) + end + + UI.important 'Copy development .cer & .p12 files' + dev_certs = Dir.entries("#{SIGNING_DIR}/raw/dev").select { |f| f.end_with?('cer') || f.end_with?('p12') } + dev_certs.each do |cert| + system("cp -f #{SIGNING_DIR}/raw/dev/#{cert} #{SIGNING_DIR}/#{cert}") + end + + if options[:release] + UI.important 'Create release provisioning profile' + release_profiles = Dir.entries("#{SIGNING_DIR}/raw/release").select { |f| f.end_with?('mobileprovision') } + RELEASE_BUNDLE_IDS.each do |id| + name = id.delete_prefix('io.leka.apf.app.') + UI.message "App: #{name} --> Bundle ID: #{id}" + release_profiles.select { |profile| profile.include?(name) }.map do |profile| + UI.message "App: #{name} --> Profile: #{profile}" + if profile.include?('AppStore_') + system("cp -f #{SIGNING_DIR}/raw/release/#{profile} #{SIGNING_DIR}/#{name}.Release.mobileprovision") + end + end + end + + UI.important 'Copy release .cer & .p12 files' + release_certs = Dir.entries("#{SIGNING_DIR}/raw/release").select do |f| + f.end_with?('cer') || f.end_with?('p12') + end + release_certs.each do |cert| + system("cp -f #{SIGNING_DIR}/raw/release/#{cert} #{SIGNING_DIR}/#{cert}") + end + end + + UI.message 'List newly created certificates and profiles' + system("ls -la #{SIGNING_DIR}") + end + + desc 'Sync certificates' + lane :sync_certificates do |options| + UI.important 'Delete local provisioning profiles' + system("rm -rf #{SIGNING_DIR}") + + create_keychain( + name: 'io.leka.fastlane.keychain', + timeout: 0, + unlock: true, + password: ENV.fetch('FASTLANE_KEYCHAIN_PASSWORD', nil) + ) + + match( + type: 'development', + app_identifier: DEVELOPMENT_WILDCARD_BUNDLE_ID, + readonly: true, + output_path: "#{SIGNING_DIR}/raw/dev", + keychain_name: 'io.leka.fastlane.keychain' + ) + + if options[:release] + match( + type: 'appstore', + app_identifier: RELEASE_BUNDLE_IDS, + readonly: true, + output_path: "#{SIGNING_DIR}/raw/release", + keychain_name: 'io.leka.fastlane.keychain' + ) + end + + copy_certificates options + end + + desc 'Create certificates' + lane :create_certificates do |options| + create_keychain( + name: 'io.leka.fastlane.keychain', + timeout: 0, + unlock: true + ) + + match( + type: 'development', + app_identifier: DEVELOPMENT_WILDCARD_BUNDLE_ID, + force_for_new_devices: true, + keychain_name: 'io.leka.fastlane.keychain' + ) + + if options[:release] + match( + type: 'appstore', + app_identifier: RELEASE_BUNDLE_IDS, + keychain_name: 'io.leka.fastlane.keychain' + ) + end + end + + desc 'Submit new internal beta app' + lane :beta_internal do |options| + UI.user_error! 'A target must be specified, for example: LekaApp, LekaUpdater, etc.' if options[:targets].nil? + + apps = [] + + UI.header 'Step: Set targets to build' + if options[:targets] == 'all' + apps = RELEASE_BUNDLE_IDS.map { |app| File.extname(app.to_s).delete('.') } + UI.important 'All targets will be built:' + else + apps += options[:targets].split(',') + UI.important 'The following targets will be built:' + end + UI.important apps.to_s + + build_date = Time.new.strftime('%Y_%m_%d') + + apps.each do |app| + UI.header "Step: Generate, build & upload target: #{app}" + Dir.chdir('..') do + ENV['TUIST_TURN_OFF_LINTERS'] = 'TRUE' + system('tuist', 'generate', '-n', app.to_s) + end + + app_id = "io.leka.apf.app.#{app}" + app_xcodeproj = "#{ROOT_DIR}/Apps/#{app}/#{app}.xcodeproj" + + begin + if ENV['CI'] + app_store_connect_api_key( + key_id: ENV.fetch('APP_STORE_CONNECT_API_KEY_ID', nil), + issuer_id: ENV.fetch('APP_STORE_CONNECT_ISSUER_ID', nil), + key_content: ENV.fetch('APP_STORE_CONNECT_API_KEY_CONTENT', nil) + ) + + unlock_keychain( + path: 'io.leka.fastlane.keychain', + password: ENV.fetch('FASTLANE_KEYCHAIN_PASSWORD', nil) + ) + + UI.header 'Step: keychain security set-key-partition-list' + system('security', 'set-key-partition-list', '-S', 'apple-tool:,apple:', '-s', '-k', + ENV.fetch('FASTLANE_KEYCHAIN_PASSWORD', nil), 'io.leka.fastlane.keychain') + end + + latest_build_number = latest_testflight_build_number(app_identifier: app_id.to_s).to_s + next_build_number_major = latest_build_number.split('.').first.to_i + 1 + pr_number = ENV.fetch('PR_NUMBER', `gh pr view --json number -q .number`.strip) + last_commit_sha_to_int = last_git_commit[:abbreviated_commit_hash].to_i(16) + + next_build_number = "#{next_build_number_major}.#{pr_number or '000'}.#{last_commit_sha_to_int}" + version_number = get_version_number(xcodeproj: app_xcodeproj.to_s) + + UI.important "App: #{app} / Version number: #{version_number} / Build number: #{next_build_number}" + + testflight_changelog = generate_changelogs( + target: app, + version_number: version_number, + latest_build_number: latest_build_number, + next_build_number: next_build_number, + pr_number: pr_number + ) + + increment_build_number( + build_number: next_build_number, + xcodeproj: app_xcodeproj.to_s + ) + + build_app( + workspace: 'ios-monorepo.xcworkspace', + scheme: app, + output_directory: './.build', + output_name: "#{build_date}-#{app}-v#{version_number}-#{next_build_number}" + ) + + upload_to_testflight( + app_identifier: app_id.to_s, + skip_waiting_for_build_processing: true, + distribute_external: false, + groups: %w[LekaTeam ClubExpert], + changelog: testflight_changelog + ) + ensure + next unless ENV['CI'] + + delete_keychain( + name: 'io.leka.fastlane.keychain' + ) + end + end + end + + desc 'Release new app version to App Store Connect' + lane :release do |options| + UI.user_error! 'A target must be specified, for example: LekaApp, LekaUpdater, etc.' if options[:target].nil? + UI.user_error! 'Only one app can be released at a time' if options[:target].split(',').count > 1 + + app = options[:target].split(',').first + + UI.header 'Step: Set target to build' + UI.important "The following targets will be built: #{app}" + + build_date = Time.new.strftime('%Y_%m_%d') + + UI.header "Step: Generate, build & upload target: #{app}" + Dir.chdir('..') do + ENV['TUIST_TURN_OFF_LINTERS'] = 'TRUE' + system('tuist', 'generate', '-n', app.to_s) + end + + app_id = "io.leka.apf.app.#{app}" + app_xcodeproj = "#{ROOT_DIR}/Apps/#{app}/#{app}.xcodeproj" + + begin + if ENV['CI'] + app_store_connect_api_key( + key_id: ENV.fetch('APP_STORE_CONNECT_API_KEY_ID_RELEASE_APP_STORE', nil), + issuer_id: ENV.fetch('APP_STORE_CONNECT_ISSUER_ID', nil), + key_content: ENV.fetch('APP_STORE_CONNECT_API_KEY_CONTENT_RELEASE_APP_STORE', nil) + ) + + unlock_keychain( + path: 'io.leka.fastlane.keychain', + password: ENV.fetch('FASTLANE_KEYCHAIN_PASSWORD', nil) + ) + + UI.header 'Step: keychain security set-key-partition-list' + system('security', 'set-key-partition-list', '-S', 'apple-tool:,apple:', '-s', '-k', + ENV.fetch('FASTLANE_KEYCHAIN_PASSWORD', nil), 'io.leka.fastlane.keychain') + end + + latest_build_number = latest_testflight_build_number(app_identifier: app_id.to_s).to_s + next_build_number = latest_build_number.split('.').first.to_i + 1 + pr_number = ENV.fetch('PR_NUMBER', `gh pr view --json number -q .number`.strip) + last_commit_sha_to_int = last_git_commit[:abbreviated_commit_hash].to_i(16) + + build_number = "#{next_build_number}.#{pr_number or '000'}.#{last_commit_sha_to_int}" + version_number = get_version_number(xcodeproj: app_xcodeproj.to_s) + + UI.important "App: #{app} / Version number: #{version_number} / Build number: #{build_number}" + UI.important "Metadata path: #{ROOT_DIR}/fastlane/release/#{app}/metadata" + + # capture_screenshots # ? not working yet + + increment_build_number( + build_number: build_number, + xcodeproj: app_xcodeproj.to_s + ) + + build_app( + workspace: 'ios-monorepo.xcworkspace', + scheme: app, + output_directory: './.build', + output_name: "#{build_date}-#{app}-v#{version_number}-#{build_number}" + ) + + upload_to_app_store( + force: true, + app_identifier: app_id.to_s, + metadata_path: "#{ROOT_DIR}/fastlane/release/#{app}/metadata", + screenshots_path: "#{ROOT_DIR}/fastlane/release/#{app}/screenshots", + reject_if_possible: true, + app_rating_config_path: "#{ROOT_DIR}/fastlane/release/#{app}/metadata/app_rating_config.json", + submission_information: { + export_compliance_uses_encryption: false, + add_id_info_uses_idfa: false, + content_rights_contains_third_party_content: true, + content_rights_has_rights: true + }, + run_precheck_before_submit: true, + precheck_include_in_app_purchases: false + ) + ensure + next unless ENV['CI'] + + delete_keychain( + name: 'io.leka.fastlane.keychain' + ) + end + end + + desc 'Generate changelogs for TestFlight, Github and Slack' + lane :generate_changelogs do |options| + UI.important options + + app_name = options[:target].to_s + version_number = options[:version_number].to_s + latest_build_number = options[:latest_build_number].to_s + next_build_number = options[:next_build_number].to_s + + pr_number = options[:pr_number] + + commit_sha_pr_branch_head = last_git_commit[:abbreviated_commit_hash] + commit_sha_latest_test_flight_build = latest_build_number.split('.').last.to_i.to_s(16).rjust(8, '0') + + UI.message "commit_sha_pr_branch_head: #{commit_sha_pr_branch_head}" + UI.message "commit_sha_latest_test_flight_build: #{commit_sha_latest_test_flight_build}" + + git_log_to_notes_script = "#{ROOT_DIR}/Tools/Scripts/git_log_to_notes.py" + + git_log_main_pr_with_emojis, stderr_git_log_main_pr_with_emojis, status_git_log_main_pr_with_emojis = + Open3.capture3( + 'python3', + git_log_to_notes_script, + '-f', 'main', + '-l', commit_sha_pr_branch_head.to_s + ) + + git_log_latest_build_pr_branch_with_emojis, _, status_git_log_latest_build_pr_branch_with_emojis = + Open3.capture3( + 'python3', + git_log_to_notes_script, + '-f', commit_sha_latest_test_flight_build.to_s, + '-l', commit_sha_pr_branch_head.to_s + ) + + git_log_main_pr_no_emojis, stderr_git_log_main_pr_no_emojis, status_git_log_main_pr_no_emojis = + Open3.capture3( + 'python3', + git_log_to_notes_script, + '-r', + '-f', 'main', + '-l', commit_sha_pr_branch_head.to_s + ) + + git_log_latest_build_pr_branch_no_emojis, _, status_git_log_latest_build_pr_branch_no_emojis = + Open3.capture3( + 'python3', + git_log_to_notes_script, + '-r', + '-f', commit_sha_latest_test_flight_build.to_s, + '-l', commit_sha_pr_branch_head.to_s + ) + + if status_git_log_main_pr_with_emojis.success? && status_git_log_main_pr_no_emojis.success? + UI.message 'Creating CHANGELOG_FOR_SLACK' + + CHANGELOG_FOR_SLACK = <<~EOF_CHANGELOG_FOR_SLACK + :test_tube: *New TestFlight Build - #{app_name}* :airplane: + + New version of #{app_name} is available for testing on TestFlight. Please check the changelog and install the app on your device. :rocket: + + *App*: #{app_name} + *Version*: #{version_number} + *Build*: #{next_build_number} (#{commit_sha_pr_branch_head}) + *PR*: #{pr_number ? "https://github.com/leka/ios-monorepo/pull/#{pr_number}" : 'n/a'} + + *Changes compared to `main`*: + + #{git_log_main_pr_with_emojis.rstrip.empty? ? '_Could not fetch the changes compared to `main`._' : git_log_main_pr_with_emojis.rstrip} + + *Changes compared to the last TestFlight build*: + + #{status_git_log_latest_build_pr_branch_with_emojis.success? ? git_log_latest_build_pr_branch_with_emojis : '_Could not fetch the changes compared to the last TestFlight build. Commit may have been overwritten by force push._'} + + EOF_CHANGELOG_FOR_SLACK + + UI.message "CHANGELOG_FOR_SLACK:\n#{CHANGELOG_FOR_SLACK}" + + UI.message 'Creating CHANGELOG_FOR_GITHUB' + + CHANGELOG_FOR_GITHUB = <<~EOF_CHANGELOG_FOR_GITHUB + ## :test_tube: New TestFlight Build - #{app_name} :airplane: + + New version of #{app_name} is available for testing on TestFlight. Please check the changelog and install the app on your device. :rocket: + + **App**: #{app_name} + **Version**: #{version_number} + **Build**: #{next_build_number} (#{commit_sha_pr_branch_head}) + **PR**: #{pr_number ? "https://github.com/leka/ios-monorepo/pull/#{pr_number}" : 'n/a'} + + **Changes compared to `main`**: + + #{git_log_main_pr_with_emojis.rstrip.empty? ? '*Could not fetch the changes compared to `main`.*' : git_log_main_pr_with_emojis.rstrip} + + **Changes compared to the last TestFlight build**: + + #{status_git_log_latest_build_pr_branch_with_emojis.success? ? git_log_latest_build_pr_branch_with_emojis : '*Could not fetch the changes compared to the last TestFlight build. Commit may have been overwritten by force push.*'} + + EOF_CHANGELOG_FOR_GITHUB + + UI.message "CHANGELOG_FOR_GITHUB:\n#{CHANGELOG_FOR_GITHUB}" + + UI.message 'Creating CHANGELOG_FOR_TEST_FLIGHT' + + CHANGELOG_FOR_TEST_FLIGHT = <<~EOF_CHANGELOG_FOR_TEST_FLIGHT + Version: #{version_number} + Build: #{next_build_number} (#{commit_sha_pr_branch_head}) + PR: #{pr_number ? "https://github.com/leka/ios-monorepo/pull/#{pr_number}" : 'n/a'} + + Changes compared to main: + + #{git_log_main_pr_no_emojis.rstrip.empty? ? 'Could not fetch the changes compared to `main`.' : git_log_main_pr_no_emojis.rstrip} + + Changes compared to the last TestFlight build: + + #{status_git_log_latest_build_pr_branch_no_emojis.success? ? git_log_latest_build_pr_branch_no_emojis : 'Could not fetch the changes compared to the last TestFlight build. Commit may have been overwritten by force push.'} + + EOF_CHANGELOG_FOR_TEST_FLIGHT + + UI.message "CHANGELOG_FOR_TEST_FLIGHT:\n#{CHANGELOG_FOR_TEST_FLIGHT}" + else + UI.error 'Could not fetch the changes' + UI.error stderr_git_log_main_pr_with_emojis + UI.error stderr_git_log_main_pr_no_emojis + end + + if ENV['CI'] + File.write( + ENV.fetch('GITHUB_ENV', nil).to_s, + "CHANGELOG_FOR_SLACK<