diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..efc5d00 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,39 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Always perform LF normalization on these files +*.c text +*.cc text +*.cmake text +*.dart text +*.gradle text +*.h text +*.html text +*.java text +*.json text +*.md text +*.py text +*.sh text +*.swift text +*.txt text +*.xml text +*.yaml text + +# Make sure that these Windows files always have CRLF line endings in checkout +*.bat text eol=crlf +*.ps1 text eol=crlf +*.rc text eol=crlf +*.sln text eol=crlf +*.props text eol=crlf +*.vcxproj text eol=crlf +*.vcxproj.filters text eol=crlf +# Including templatized versions. +*.sln.tmpl text eol=crlf +*.props.tmpl text eol=crlf +*.vcxproj.tmpl text eol=crlf + +# Never perform LF normalization on these files +*.ico binary +*.jar binary +*.png binary +*.zip binary diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..aeadb9e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,20 @@ +*Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.* + +*List which issues are fixed by this PR. For larger changes, raising an issue first helps +reduce redundant work.* + +## Pre-launch Checklist + +- [ ] I read the [Flutter Style Guide] _recently_, and have followed its advice. +- [ ] I signed the [CLA]. +- [ ] I read the [Contributors Guide]. +- [ ] I updated/added relevant documentation (doc comments with `///`). +- [ ] All existing and new tests are passing. + +If you need help, consider asking for advice on the #hackers-devrel channel on [Discord]. + + +[Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo +[CLA]: https://cla.developers.google.com/ +[Discord]: https://github.com/flutter/flutter/wiki/Chat +[Contributors Guide]: https://github.com/flutter/games/blob/main/CONTRIBUTING.md diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..e35d262 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,15 @@ +version: 2 +enable-beta-ecosystems: true +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + labels: + - "autosubmit" + - package-ecosystem: "pub" + directory: "templates/basic/" + schedule: + interval: "daily" + labels: + - "autosubmit" diff --git a/.github/no-response.yml b/.github/no-response.yml new file mode 100644 index 0000000..c712897 --- /dev/null +++ b/.github/no-response.yml @@ -0,0 +1,16 @@ +# Configuration for probot-no-response - https://github.com/probot/no-response + +# Number of days of inactivity before an issue is closed for lack of response. +daysUntilClose: 14 + +# Label requiring a response. +responseRequiredLabel: "needs-info" + +# Comment to post when closing an Issue for lack of response. +closeComment: >- + Without additional information we're not able to resolve this issue, + so it will be closed at this time. You're still free to add more info + and respond to any questions above, though. We'll reopen the case + if you do. + + Thanks for your contribution! diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml new file mode 100644 index 0000000..41a9f3d --- /dev/null +++ b/.github/workflows/beta.yml @@ -0,0 +1,37 @@ +name: Beta Branch CI + +# Declare default permissions as read only. +permissions: read-all + +on: + push: + branches: [beta] + pull_request: + branches: [beta] + workflow_dispatch: + +defaults: + run: + shell: bash + +jobs: + # Run the stable test script on the beta channel. Since this branch will soon + # be merged into main as our stable-targeting code, this is the key thing we + # need to test. + stable-tests-on-beta: + runs-on: ${{ matrix.os }} + if: github.repository == 'flutter/samples' + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 + with: + distribution: 'zulu' + java-version: '17' + - uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 + with: + channel: beta + - run: ./flutter_ci_script_stable.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..c830a0b --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,38 @@ +name: Main Branch CI + +# Declare default permissions as read only. +permissions: read-all + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + schedule: + - cron: "0 0 * * *" # Every day at midnight + +defaults: + run: + shell: bash + +jobs: + flutter-tests: + name: Test Flutter ${{ matrix.flutter_version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + if: github.repository == 'flutter/samples' + strategy: + fail-fast: false + matrix: + flutter_version: [stable, beta] + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 + with: + distribution: 'zulu' + java-version: '17' + - uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 + with: + channel: ${{ matrix.flutter_version }} + - run: ./flutter_ci_script_${{ matrix.flutter_version }}.sh diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml new file mode 100644 index 0000000..a868fcf --- /dev/null +++ b/.github/workflows/scorecards-analysis.yml @@ -0,0 +1,55 @@ +name: Scorecards supply-chain security +on: + # Only the default branch is supported. + branch_protection_rule: + push: + branches: [ main ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecards analysis + runs-on: ubuntu-latest + if: ${{ github.repository == 'flutter/samples' }} + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + actions: read + contents: read + id-token: write + + steps: + - name: "Checkout code" + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 + with: + results_file: results.sarif + results_format: sarif + # Read-only PAT token. To create it, + # follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation. + repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} + # Publish the results to enable scorecard badges. For more details, see + # https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories, `publish_results` will automatically be set to `false`, + # regardless of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). + - name: "Upload artifact" + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@cdcdbb579706841c47f7063dda365e292e5cad7a + with: + sarif_file: results.sarif diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee83909 --- /dev/null +++ b/.gitignore @@ -0,0 +1,83 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Android related +**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.* + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Web related +**/web/**/lib/generated_plugin_registrant.dart + +# Service account files +svc-keyfile.json + +# Lockfiles +Podfile.lock +pubspec.lock +yarn.lock + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..8e565f3 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,7 @@ +# Below is a list of people and organizations that have contributed +# to the project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +Filip Hracek diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..881b5ed --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,129 @@ +# Contributing + +_See also: [Flutter's code of conduct](https://github.com/flutter/flutter/blob/master/CODE_OF_CONDUCT.md)_ + +Want to contribute to the Flutter games sample ecosystem? Great! First, read this +page (including the small print at the end). + +## Is this the right place for your contribution? + +This repo is used by members of the Flutter team and a few partners as a place +to store example games and game templates. It's not meant to be the one and only +source of truth for Flutter game samples or the only place people go to learn +about the best ways to build games with Flutter. What that means in practice +is that if you've written a great example game, it doesn't need to be maintained +here in order to get noticed, be of help to new Flutter devs, and have an impact +on the community. + +You can maintain your sample app in your own repo (or with another source +control provider) and still be as important a part of the Flutter-verse as +anything you see here. You can let us know on the +[FlutterDev Google Group](https://groups.google.com/forum/#!forum/flutter-dev) +when you've published something and post about it with the +`#FlutterDev` hashtag. + +## So what should be contributed here, then? + +Fixes and necessary improvements to the existing samples, mostly. + +## Before you contribute + +### File an issue first! + +If you see a bug or have an idea for a feature that you feel would improve one +of the samples already in the repo, **please +[file an issue](https://github.com/flutter/games/issues/new) before you begin +coding or send a PR**. This will help prevent duplicate work by letting us know +what you're up to. It will help avoid a situation in which you spend a lot of +time coding something that's not quite right for the repo or its goals. + +### Sign the license agreement. + +Before we can use your code, you must sign the +[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) +(CLA), which you can do online. The CLA is necessary mainly because you own the +copyright to your changes, even after your contribution becomes part of our +codebase, so we need your permission to use and distribute your code. We also +need to be sure of various other things—for instance that you'll tell us if you +know that your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. + +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. + +## A few ground rules + +This is a monorepo containing a bunch of projects. While different codebases have +different needs, there are a few basic conventions that every project here is +expected to follow. All of them are based on our experience over the last +couple years keeping similar repos tidy and running smooth. + +Each app should: + +* Be designed to build against the current + [stable](https://github.com/flutter/flutter/wiki/Flutter-build-release-channels) + release of the Flutter SDK. +* Include the top level + [`analysis_options.yaml`](analysis_options.yaml) + file used throughout the repo. This file include a base set of analyzer + conventions and lints. +* Have no analyzer errors or warnings. +* Be formatted with `dart format`. +* Include at least one working test in its `test` folder. +* Be wired into the list of projects in the CI scripts for [stable](tool/flutter_ci_script_stable.sh), + [beta](tool/flutter_ci_script_beta.sh), and [master](tool/flutter_ci_script_master.sh), + which runs the analyzer, the formatter, and `flutter test`. +* Add the new project directory to the [dependabot](.github/dependabot.yaml) configuration + to keep dependencies updated in an on-going basis. +* Avoid adding an onerous amount of blobs (typically images or other assets) to + the repo. + +In addition, sample code is, at the end of the day, still code. It should be +written with at least as much care as the Flutter code you'd find in the SDK +itself. For that reason, most of the +[Flutter style guide](https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo) +also applies to code in this repo. + +## Code reviews + +All submissions, including submissions by project members, require review. + +This repo is part of the [Flutter](https://github.com/flutter) GitHub account, +which means that a lot of folks have the ability to push and merge code. The +primary maintainers, though, are: + +* [@filiph](https://github.com/filiph) +* [@domesticmouse](https://github.com/domesticmouse) + +You are free to add one of these folks as a reviewer +to any PR sent to this repo. We're happy to comment, answer (or ask) questions, +and provide feedback. + +If you're part of a team that's already landed a sample in the repo, +and you're updating or fixing that sample, you are *not* expected to +wait on one of the above folks before merging the code. Have it reviewed by +someone you trust on your own team, and then merge it. + +However, if you're adding a new sample, updating a sample you've never worked on +before, or changing something that's a meta-concern like the CI setup, web +hosting, project setup, etc., please include one of the primary maintainers as a +reviewer. + +## File headers + +All files in the project must start with the following header. + +``` +// Copyright 2023 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +``` + +## The small print + +Contributions made by corporations are covered by a different agreement than the +one above, the +[Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..57e60e3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,326 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- +Copyright (c) 2014, Vaibhav Singh (design) and Rosetta Type Foundry s.r.o. (post-production). + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +-------------------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + 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. diff --git a/README.md b/README.md index 8633398..6028c8d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # games +[![Build Status](https://github.com/flutter/games/workflows/Main%20Branch%20CI/badge.svg)](https://github.com/flutter/games/actions?workflow=Main%20Branch%20CI) + Home to the official code for the Flutter Casual Games Toolkit and other Flutter gaming templates. @@ -7,3 +9,97 @@ Visit [flutter.dev/games](https://flutter.dev/games) for more information. If you're new to Flutter, you first need to install the [Flutter SDK](https://flutter.dev/). + + +## Organization + +The projects in this repository are divided into two broad categories: + +1. **Templates.** A small number of starting points for your game project. + Templates are intended to give you a simple project structure + on top of which you can build your game. + They differ to accomodate different game types, + from the simplest casual games through drag&drop board games + to 2D platformers. + +2. **Samples.** Intended to show functionality beyond the basics, + such as multiplayer or ads integration. + + +```text +. +├── templates +│ ├── basic +│ ├── card +│ └── endless_runner +└── samples + ├── ads + ├── multiplayer + └── ... +``` + + +## How to use + +1. Clone this repository. + + To minimize download size, you can use a + [partial clone](https://github.blog/2020-12-21-get-up-to-speed-with-partial-clone-and-shallow-clone/): + + ```shell + git clone --filter=blob:none https://github.com/flutter/games.git + ``` + + (Alternatively, you can + [download the ZIP file](https://github.com/flutter/games/archive/refs/heads/main.zip) + and uncompress it on your workstation.) + +2. `cd` into the project you are interested in. For example: + + ```shell + cd templates/card_game + ``` + + (Alternatively, you can copy the contents of a project to somewhere else, + and change to that directory.) + +3. Let Flutter create the platform subdirectories: + + ```shell + flutter create . + ``` + + (If you have changed the name of the project's directory to something else, + you will need to tell the Flutter tool the original project name + with something like `flutter create --project-name card_game .`.). + + + +4. (Optional) Rename the project with the + [`rename` tool](https://pub.dev/packages/rename). + +5. Open the project in your favorite editor and enjoy. + + +## Interested in contributing? + +See the [contributor's guide](CONTRIBUTING.md)! + + +## Questions or issues? + +If you have a general question about one of these samples or how to adapt its +techniques for one of your own apps, try one of these resources: + +* [The FlutterDev Google Group](https://groups.google.com/forum/#!forum/flutter-dev) +* [The FlutterDev Discord](https://discord.gg/rflutterdev) +* [The Flame Discord](https://discord.com/invite/pxrBmy4) +* [StackOverflow](https://stackoverflow.com/questions/tagged/flutter) + +If you run into a bug in one of the samples, please file an issue in the +[`flutter/games` issue tracker](https://github.com/flutter/games/issues). + diff --git a/flutter_ci_script_beta.sh b/flutter_ci_script_beta.sh new file mode 100644 index 0000000..23d3ae1 --- /dev/null +++ b/flutter_ci_script_beta.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +DIR="${BASH_SOURCE%/*}" +source "$DIR/flutter_ci_script_shared.sh" + +flutter doctor -v + +declare -ar PROJECT_NAMES=( + "templates/basic" + "templates/card" +) + +ci_projects "beta" "${PROJECT_NAMES[@]}" + +echo "-- Success --" diff --git a/flutter_ci_script_shared.sh b/flutter_ci_script_shared.sh new file mode 100644 index 0000000..2842c94 --- /dev/null +++ b/flutter_ci_script_shared.sh @@ -0,0 +1,33 @@ +function ci_projects () { + local channel="$1" + + shift + local arr=("$@") + for PROJECT_NAME in "${arr[@]}" + do + echo "== Testing '${PROJECT_NAME}' on Flutter's $channel channel ==" + pushd "${PROJECT_NAME}" + + # Grab packages. + flutter pub get + + # Run the analyzer to find any static analysis issues. + dart analyze --fatal-infos + + # Run the formatter on all the dart files to make sure everything's linted. + dart format --output none --set-exit-if-changed . + + # Run the actual tests. + if [ -d "test" ] + then + if grep -q "flutter:" "pubspec.yaml"; then + flutter test + else + # If the project is not a Flutter project, use the Dart CLI. + dart test + fi + fi + + popd + done +} diff --git a/flutter_ci_script_stable.sh b/flutter_ci_script_stable.sh new file mode 100644 index 0000000..31a209a --- /dev/null +++ b/flutter_ci_script_stable.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +DIR="${BASH_SOURCE%/*}" +source "$DIR/flutter_ci_script_shared.sh" + +flutter doctor -v + +declare -ar PROJECT_NAMES=( + "templates/basic" + "templates/card" +) + +ci_projects "stable" "${PROJECT_NAMES[@]}" + +echo "-- Success --" diff --git a/templates/basic/.gitignore b/templates/basic/.gitignore new file mode 100644 index 0000000..24476c5 --- /dev/null +++ b/templates/basic/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/templates/basic/.metadata b/templates/basic/.metadata new file mode 100644 index 0000000..80c05e2 --- /dev/null +++ b/templates/basic/.metadata @@ -0,0 +1,36 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d211f42860350d914a5ad8102f9ec32764dc6d06" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + - platform: android + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + - platform: ios + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + - platform: web + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/templates/basic/README.md b/templates/basic/README.md new file mode 100644 index 0000000..dbc158e --- /dev/null +++ b/templates/basic/README.md @@ -0,0 +1,244 @@ +A starter Flutter project with a minimal shell of a game +including the following features: + +- main menu screen +- basic navigation +- game-y theming +- settings +- sound + +You can jump directly into building your game in `lib/play_session/`. + +When you're ready for things like ads, in-app purchases, achievements, +analytics, crash reporting, and so on, +there are resources ready for you +at [flutter.dev/games](https://flutter.dev/games). + + +# Getting started + +Clone this project and run the following command in its root directory: + +```terminal +flutter create . --project-name basic +``` + +This will create the necessary platform files, such as `ios/`, `android/`, +`web/`, `macos/`, `linux/` or `windows/`, depending on your installation of Flutter. + +After this, the game compiles and works out of the box. It comes with things +like a main menu, a router, a settings screen, and audio. +When building a new game, this is likely everything you first need. + +When you're ready to enable more advanced integrations, +there are recipes and codelabs for you ready at +[flutter.dev/games](https://flutter.dev/games). + + +# Development + +To run the app in debug mode: + + flutter run + +It is often convenient to develop your game as a desktop app. +For example, you can run `flutter run -d macOS`, and get the same UI +in a desktop window on a Mac. That way, you don't need to use a +simulator/emulator or attach a mobile device. + + +## Code organization + +Code is organized in a loose and shallow feature-first fashion. +In `lib/`, you'll therefore find directories such as `audio`, +`main_menu` or `settings`. Nothing fancy, but usable. + +```text +lib +├── app_lifecycle +├── audio +├── game_internals +├── level_selection +├── main_menu +├── play_session +├── player_progress +├── settings +├── style +├── win_game +│ +├── main.dart +└── router.dart +``` + +The state management approach is intentionally low-level. That way, it's easy to +take this project and run with it, without having to learn new paradigms, or having +to remember to run `flutter pub run build_runner watch`. You are, +of course, encouraged to use whatever paradigm, helper package or code generation +scheme that you prefer. + + +## Building for production + +To build the app for iOS (and open Xcode when finished): + +```shell +flutter build ipa && open build/ios/archive/Runner.xcarchive +``` + +To build the app for Android (and open the folder with the bundle when finished): + +```shell +flutter build appbundle && open build/app/outputs/bundle/release +``` + +While the template is primarily meant for mobile games, you can also publish +for the web. This might be useful for web-based demos, for example, +or for rapid play-testing. The following command requires installing +[`peanut`](https://pub.dev/packages/peanut/install). + +```bash +flutter pub global run peanut \ +--web-renderer canvaskit \ +--extra-args "--base-href=/name_of_your_github_repo/" \ +&& git push origin --set-upstream gh-pages +``` + +The last line of the command above automatically pushes +your newly built web game to GitHub pages, assuming that you have +that set up. + +Lastly, it is of course possible to build your game for desktop platforms: +Windows, Linux and macOS. +Follow the [standard instructions](https://docs.flutter.dev/platform-integration/desktop). + + +# Integrations + +Focus on making your core gameplay fun first. Don't worry about +integrations like ads, in-app purchases, analytics, and so on. +It's easy to add them later, and you can find recipes and codelabs +for them at [flutter.dev/games](https://flutter.dev/games). + +Change the package name of your game +before you start any of the deeper integrations. +[StackOverflow has instructions](https://stackoverflow.com/a/51550358/1416886) +for this, and the [`rename`](https://pub.dev/packages/rename) tool +(on pub.dev) automates the process. + + +## Audio + +Audio is enabled by default and ready to go. You can modify code +in `lib/audio/` to your liking. + +You can find some music +tracks in `assets/music` — these are Creative Commons Attribution (CC-BY) +licensed, and are included in this repository with permission. If you decide +to keep these tracks in your game, please don't forget to give credit +to the musician, [Mr Smith][]. + +[Mr Smith]: https://freemusicarchive.org/music/mr-smith + +The repository also includes a few sound effect samples in `assets/sfx`. +These are public domain (CC0) and you will almost surely want to replace +them because they're just recordings of a developer doing silly sounds +with their mouth. + +## Logging + +The template uses the [`logging`](https://pub.dev/packages/logging) package +to log messages to the console. This makes it very easy to log messages +from anywhere with something like the following: + +```dart +import 'package:logging/logging.dart'; + +final _log = Logger('Foo'); + +void foo() { + _log.info('Hello, world!'); +} +``` + +This will show up in the console as: + +```text +[Foo] Hello, world! +``` + +When using Flutter DevTools, all the metadata of the log message is preserved, +so you can filter by logger name, log level, and so on. + +Later, when you're closer to production, you can gather these log messages +(see `lib/main.dart`) and send them to a service like Firebase Crashlytics +when appropriate. +See [`firebase_crashlytics`](https://pub.dev/packages/firebase_crashlytics) +for more information. + + +## Settings + +The settings page is enabled by default, and accessible both +from the main menu and through the "gear" button in the play session screen. + +Settings are saved to local storage using the +[`shared_preferences`](https://pub.dev/packages/shared_preferences) +package. +To change what preferences are saved and how, edit files in +`lib/settings/persistence`. + + +# Icon + +To update the launcher icon, first change the files +`assets/icon-adaptive-foreground.png` and `assets/icon.png`. +Then, run the following: + +```bash +dart run flutter_launcher_icons:main +``` + +You can [configure](https://github.com/fluttercommunity/flutter_launcher_icons#book-guide) +the look of the icon in the `flutter_icons:` section of `pubspec.yaml`. + + +# Troubleshooting + +## CocoaPods + +When upgrading to higher versions of Flutter or plugins, you might encounter an error when +building the iOS or macOS app. A good first thing to try is to delete the `ios/Podfile.lock` +file (or `macos/Podfile.lock`, respectively), then trying to build again. (You can achieve +a more thorough cleanup by running `flutter clean` instead.) + +If this doesn't help, here are some more methods: + +- See if everything is still okay with your Flutter and CocoaPods installation + by running `flutter doctor`. Revisit the macOS + [Flutter installation guide](https://docs.flutter.dev/get-started/install/macos) + if needed. +- Update CocoaPods specs directory: + + ```sh + cd ios + pod repo update + cd .. + ``` + + (Substitute `ios` for `macos` when appropriate.) +- Open the project in Xcode, + [increase the build target](https://stackoverflow.com/a/38602597/1416886), + then select _Product_ > _Clean Build Folder_. + +## Warnings in console + +When running the game for the first time, you might see warnings like the following: + +> Note: Some input files use or override a deprecated API. + +or + +> warning: 'viewState' was deprecated in macOS 11.0: Use -initWithState: instead + +These warning come from the various plugins that are used by the template. They are not harmful +and can be ignored. The warnings are meant for the plugin authors, not for you, the game developer. diff --git a/templates/basic/analysis_options.yaml b/templates/basic/analysis_options.yaml new file mode 100644 index 0000000..12b7a31 --- /dev/null +++ b/templates/basic/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + # Remove or force lint rules by adding lines like the following. + # The lints below are disabled in order to make things smoother in early + # development. Consider enabling them once development is further along. + prefer_const_constructors: false # Annoying in early development + prefer_single_quotes: false # Annoying in early development diff --git a/templates/basic/android/.gitignore b/templates/basic/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/templates/basic/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/templates/basic/android/app/build.gradle b/templates/basic/android/app/build.gradle new file mode 100644 index 0000000..0b96401 --- /dev/null +++ b/templates/basic/android/app/build.gradle @@ -0,0 +1,67 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + namespace "com.example.basic" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.basic" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies {} diff --git a/templates/basic/android/app/src/debug/AndroidManifest.xml b/templates/basic/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/templates/basic/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/templates/basic/android/app/src/main/AndroidManifest.xml b/templates/basic/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d79291a --- /dev/null +++ b/templates/basic/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/templates/basic/android/app/src/main/kotlin/com/example/basic/MainActivity.kt b/templates/basic/android/app/src/main/kotlin/com/example/basic/MainActivity.kt new file mode 100644 index 0000000..ec9af92 --- /dev/null +++ b/templates/basic/android/app/src/main/kotlin/com/example/basic/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.basic + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/templates/basic/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/templates/basic/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..d94bfc5 Binary files /dev/null and b/templates/basic/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ diff --git a/templates/basic/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/templates/basic/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..cf8385a Binary files /dev/null and b/templates/basic/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ diff --git a/templates/basic/android/app/src/main/res/drawable-v21/launch_background.xml b/templates/basic/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/templates/basic/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/templates/basic/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/templates/basic/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..4784408 Binary files /dev/null and b/templates/basic/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ diff --git a/templates/basic/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/templates/basic/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..175b4a7 Binary files /dev/null and b/templates/basic/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ diff --git a/templates/basic/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/templates/basic/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..e7cd1a7 Binary files /dev/null and b/templates/basic/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ diff --git a/templates/basic/android/app/src/main/res/drawable/launch_background.xml b/templates/basic/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/templates/basic/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/templates/basic/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/templates/basic/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..5f349f7 --- /dev/null +++ b/templates/basic/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/templates/basic/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/templates/basic/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..8b92f00 Binary files /dev/null and b/templates/basic/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/templates/basic/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/templates/basic/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..f82b4fa Binary files /dev/null and b/templates/basic/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/templates/basic/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/templates/basic/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..f53e3bf Binary files /dev/null and b/templates/basic/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/templates/basic/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/templates/basic/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..ae83113 Binary files /dev/null and b/templates/basic/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/templates/basic/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/templates/basic/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..d855a43 Binary files /dev/null and b/templates/basic/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/templates/basic/android/app/src/main/res/values-night/styles.xml b/templates/basic/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/templates/basic/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/templates/basic/android/app/src/main/res/values/colors.xml b/templates/basic/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/templates/basic/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/templates/basic/android/app/src/main/res/values/styles.xml b/templates/basic/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/templates/basic/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/templates/basic/android/app/src/profile/AndroidManifest.xml b/templates/basic/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/templates/basic/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/templates/basic/android/build.gradle b/templates/basic/android/build.gradle new file mode 100644 index 0000000..f7eb7f6 --- /dev/null +++ b/templates/basic/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/templates/basic/android/gradle.properties b/templates/basic/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/templates/basic/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/templates/basic/android/gradle/wrapper/gradle-wrapper.properties b/templates/basic/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3c472b9 --- /dev/null +++ b/templates/basic/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/templates/basic/android/settings.gradle b/templates/basic/android/settings.gradle new file mode 100644 index 0000000..55c4ca8 --- /dev/null +++ b/templates/basic/android/settings.gradle @@ -0,0 +1,20 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() + + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + + plugins { + id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false + } +} + +include ":app" + +apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/templates/basic/assets/fonts/Permanent_Marker/LICENSE.txt b/templates/basic/assets/fonts/Permanent_Marker/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/templates/basic/assets/fonts/Permanent_Marker/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + 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. diff --git a/templates/basic/assets/fonts/Permanent_Marker/PermanentMarker-Regular.ttf b/templates/basic/assets/fonts/Permanent_Marker/PermanentMarker-Regular.ttf new file mode 100644 index 0000000..6541e9d Binary files /dev/null and b/templates/basic/assets/fonts/Permanent_Marker/PermanentMarker-Regular.ttf differ diff --git a/templates/basic/assets/icons/icon-adaptive-foreground.png b/templates/basic/assets/icons/icon-adaptive-foreground.png new file mode 100644 index 0000000..6fe9455 Binary files /dev/null and b/templates/basic/assets/icons/icon-adaptive-foreground.png differ diff --git a/templates/basic/assets/icons/icon.png b/templates/basic/assets/icons/icon.png new file mode 100644 index 0000000..a8f5c30 Binary files /dev/null and b/templates/basic/assets/icons/icon.png differ diff --git a/templates/basic/assets/images/2x/back.png b/templates/basic/assets/images/2x/back.png new file mode 100644 index 0000000..c2daa28 Binary files /dev/null and b/templates/basic/assets/images/2x/back.png differ diff --git a/templates/basic/assets/images/2x/restart.png b/templates/basic/assets/images/2x/restart.png new file mode 100644 index 0000000..360c109 Binary files /dev/null and b/templates/basic/assets/images/2x/restart.png differ diff --git a/templates/basic/assets/images/2x/settings.png b/templates/basic/assets/images/2x/settings.png new file mode 100644 index 0000000..8780f94 Binary files /dev/null and b/templates/basic/assets/images/2x/settings.png differ diff --git a/templates/basic/assets/images/3.5x/back.png b/templates/basic/assets/images/3.5x/back.png new file mode 100644 index 0000000..5961b22 Binary files /dev/null and b/templates/basic/assets/images/3.5x/back.png differ diff --git a/templates/basic/assets/images/3.5x/restart.png b/templates/basic/assets/images/3.5x/restart.png new file mode 100644 index 0000000..3f89e19 Binary files /dev/null and b/templates/basic/assets/images/3.5x/restart.png differ diff --git a/templates/basic/assets/images/3.5x/settings.png b/templates/basic/assets/images/3.5x/settings.png new file mode 100644 index 0000000..33d4326 Binary files /dev/null and b/templates/basic/assets/images/3.5x/settings.png differ diff --git a/templates/basic/assets/images/3x/back.png b/templates/basic/assets/images/3x/back.png new file mode 100644 index 0000000..2ac088b Binary files /dev/null and b/templates/basic/assets/images/3x/back.png differ diff --git a/templates/basic/assets/images/3x/restart.png b/templates/basic/assets/images/3x/restart.png new file mode 100644 index 0000000..3578b92 Binary files /dev/null and b/templates/basic/assets/images/3x/restart.png differ diff --git a/templates/basic/assets/images/3x/settings.png b/templates/basic/assets/images/3x/settings.png new file mode 100644 index 0000000..a7dbd26 Binary files /dev/null and b/templates/basic/assets/images/3x/settings.png differ diff --git a/templates/basic/assets/images/back.png b/templates/basic/assets/images/back.png new file mode 100644 index 0000000..65ab3ee Binary files /dev/null and b/templates/basic/assets/images/back.png differ diff --git a/templates/basic/assets/images/restart.png b/templates/basic/assets/images/restart.png new file mode 100644 index 0000000..bd22efe Binary files /dev/null and b/templates/basic/assets/images/restart.png differ diff --git a/templates/basic/assets/images/settings.png b/templates/basic/assets/images/settings.png new file mode 100644 index 0000000..48863f4 Binary files /dev/null and b/templates/basic/assets/images/settings.png differ diff --git a/templates/basic/assets/music/Mr_Smith-Azul.mp3 b/templates/basic/assets/music/Mr_Smith-Azul.mp3 new file mode 100644 index 0000000..d454d1a Binary files /dev/null and b/templates/basic/assets/music/Mr_Smith-Azul.mp3 differ diff --git a/templates/basic/assets/music/Mr_Smith-Sonorus.mp3 b/templates/basic/assets/music/Mr_Smith-Sonorus.mp3 new file mode 100644 index 0000000..28a01d7 Binary files /dev/null and b/templates/basic/assets/music/Mr_Smith-Sonorus.mp3 differ diff --git a/templates/basic/assets/music/Mr_Smith-Sunday_Solitude.mp3 b/templates/basic/assets/music/Mr_Smith-Sunday_Solitude.mp3 new file mode 100644 index 0000000..0eec639 Binary files /dev/null and b/templates/basic/assets/music/Mr_Smith-Sunday_Solitude.mp3 differ diff --git a/templates/basic/assets/music/README.md b/templates/basic/assets/music/README.md new file mode 100644 index 0000000..c054c07 --- /dev/null +++ b/templates/basic/assets/music/README.md @@ -0,0 +1,6 @@ +Music in the template is by Mr Smith, and licensed under Creative Commons +Attribution 4.0 International (CC BY 4.0). + +https://freemusicarchive.org/music/mr-smith + +Mr Smith's music is used in this template project with his explicit permission. diff --git a/templates/basic/assets/sfx/README.md b/templates/basic/assets/sfx/README.md new file mode 100644 index 0000000..9009909 --- /dev/null +++ b/templates/basic/assets/sfx/README.md @@ -0,0 +1 @@ +Sounds in this folder are made by Filip Hracek and are CC0 (Public Domain). diff --git a/templates/basic/assets/sfx/dsht1.mp3 b/templates/basic/assets/sfx/dsht1.mp3 new file mode 100644 index 0000000..8ce3ac5 Binary files /dev/null and b/templates/basic/assets/sfx/dsht1.mp3 differ diff --git a/templates/basic/assets/sfx/ehehee1.mp3 b/templates/basic/assets/sfx/ehehee1.mp3 new file mode 100644 index 0000000..73d6ccf Binary files /dev/null and b/templates/basic/assets/sfx/ehehee1.mp3 differ diff --git a/templates/basic/assets/sfx/fwfwfwfw1.mp3 b/templates/basic/assets/sfx/fwfwfwfw1.mp3 new file mode 100644 index 0000000..2784578 Binary files /dev/null and b/templates/basic/assets/sfx/fwfwfwfw1.mp3 differ diff --git a/templates/basic/assets/sfx/fwfwfwfwfw1.mp3 b/templates/basic/assets/sfx/fwfwfwfwfw1.mp3 new file mode 100644 index 0000000..f847701 Binary files /dev/null and b/templates/basic/assets/sfx/fwfwfwfwfw1.mp3 differ diff --git a/templates/basic/assets/sfx/hash1.mp3 b/templates/basic/assets/sfx/hash1.mp3 new file mode 100644 index 0000000..5e4bb7c Binary files /dev/null and b/templates/basic/assets/sfx/hash1.mp3 differ diff --git a/templates/basic/assets/sfx/hash2.mp3 b/templates/basic/assets/sfx/hash2.mp3 new file mode 100644 index 0000000..1068ce4 Binary files /dev/null and b/templates/basic/assets/sfx/hash2.mp3 differ diff --git a/templates/basic/assets/sfx/hash3.mp3 b/templates/basic/assets/sfx/hash3.mp3 new file mode 100644 index 0000000..916ddda Binary files /dev/null and b/templates/basic/assets/sfx/hash3.mp3 differ diff --git a/templates/basic/assets/sfx/haw1.mp3 b/templates/basic/assets/sfx/haw1.mp3 new file mode 100644 index 0000000..7d25af2 Binary files /dev/null and b/templates/basic/assets/sfx/haw1.mp3 differ diff --git a/templates/basic/assets/sfx/hh1.mp3 b/templates/basic/assets/sfx/hh1.mp3 new file mode 100644 index 0000000..8ca4344 Binary files /dev/null and b/templates/basic/assets/sfx/hh1.mp3 differ diff --git a/templates/basic/assets/sfx/hh2.mp3 b/templates/basic/assets/sfx/hh2.mp3 new file mode 100644 index 0000000..b11cb0a Binary files /dev/null and b/templates/basic/assets/sfx/hh2.mp3 differ diff --git a/templates/basic/assets/sfx/k1.mp3 b/templates/basic/assets/sfx/k1.mp3 new file mode 100644 index 0000000..757e21e Binary files /dev/null and b/templates/basic/assets/sfx/k1.mp3 differ diff --git a/templates/basic/assets/sfx/k2.mp3 b/templates/basic/assets/sfx/k2.mp3 new file mode 100644 index 0000000..1e45f8b Binary files /dev/null and b/templates/basic/assets/sfx/k2.mp3 differ diff --git a/templates/basic/assets/sfx/kch1.mp3 b/templates/basic/assets/sfx/kch1.mp3 new file mode 100644 index 0000000..cfe79c3 Binary files /dev/null and b/templates/basic/assets/sfx/kch1.mp3 differ diff --git a/templates/basic/assets/sfx/kss1.mp3 b/templates/basic/assets/sfx/kss1.mp3 new file mode 100644 index 0000000..e7fd20a Binary files /dev/null and b/templates/basic/assets/sfx/kss1.mp3 differ diff --git a/templates/basic/assets/sfx/lalala1.mp3 b/templates/basic/assets/sfx/lalala1.mp3 new file mode 100644 index 0000000..85dcd80 Binary files /dev/null and b/templates/basic/assets/sfx/lalala1.mp3 differ diff --git a/templates/basic/assets/sfx/oo1.mp3 b/templates/basic/assets/sfx/oo1.mp3 new file mode 100644 index 0000000..172ca82 Binary files /dev/null and b/templates/basic/assets/sfx/oo1.mp3 differ diff --git a/templates/basic/assets/sfx/p1.mp3 b/templates/basic/assets/sfx/p1.mp3 new file mode 100644 index 0000000..6575546 Binary files /dev/null and b/templates/basic/assets/sfx/p1.mp3 differ diff --git a/templates/basic/assets/sfx/p2.mp3 b/templates/basic/assets/sfx/p2.mp3 new file mode 100644 index 0000000..7d7a7fd Binary files /dev/null and b/templates/basic/assets/sfx/p2.mp3 differ diff --git a/templates/basic/assets/sfx/sh1.mp3 b/templates/basic/assets/sfx/sh1.mp3 new file mode 100644 index 0000000..5059dd5 Binary files /dev/null and b/templates/basic/assets/sfx/sh1.mp3 differ diff --git a/templates/basic/assets/sfx/sh2.mp3 b/templates/basic/assets/sfx/sh2.mp3 new file mode 100644 index 0000000..6425d99 Binary files /dev/null and b/templates/basic/assets/sfx/sh2.mp3 differ diff --git a/templates/basic/assets/sfx/spsh1.mp3 b/templates/basic/assets/sfx/spsh1.mp3 new file mode 100644 index 0000000..34065c8 Binary files /dev/null and b/templates/basic/assets/sfx/spsh1.mp3 differ diff --git a/templates/basic/assets/sfx/swishswish1.mp3 b/templates/basic/assets/sfx/swishswish1.mp3 new file mode 100644 index 0000000..2d030aa Binary files /dev/null and b/templates/basic/assets/sfx/swishswish1.mp3 differ diff --git a/templates/basic/assets/sfx/wehee1.mp3 b/templates/basic/assets/sfx/wehee1.mp3 new file mode 100644 index 0000000..8f7bc84 Binary files /dev/null and b/templates/basic/assets/sfx/wehee1.mp3 differ diff --git a/templates/basic/assets/sfx/ws1.mp3 b/templates/basic/assets/sfx/ws1.mp3 new file mode 100644 index 0000000..0daf26a Binary files /dev/null and b/templates/basic/assets/sfx/ws1.mp3 differ diff --git a/templates/basic/assets/sfx/wssh1.mp3 b/templates/basic/assets/sfx/wssh1.mp3 new file mode 100644 index 0000000..9f455d9 Binary files /dev/null and b/templates/basic/assets/sfx/wssh1.mp3 differ diff --git a/templates/basic/assets/sfx/wssh2.mp3 b/templates/basic/assets/sfx/wssh2.mp3 new file mode 100644 index 0000000..b5c5c3b Binary files /dev/null and b/templates/basic/assets/sfx/wssh2.mp3 differ diff --git a/templates/basic/assets/sfx/yay1.mp3 b/templates/basic/assets/sfx/yay1.mp3 new file mode 100644 index 0000000..51102cb Binary files /dev/null and b/templates/basic/assets/sfx/yay1.mp3 differ diff --git a/templates/basic/ios/.gitignore b/templates/basic/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/templates/basic/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/templates/basic/ios/Flutter/AppFrameworkInfo.plist b/templates/basic/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/templates/basic/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/templates/basic/ios/Flutter/Debug.xcconfig b/templates/basic/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/templates/basic/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/templates/basic/ios/Flutter/Release.xcconfig b/templates/basic/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/templates/basic/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/templates/basic/ios/Podfile b/templates/basic/ios/Podfile new file mode 100644 index 0000000..fdcc671 --- /dev/null +++ b/templates/basic/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/templates/basic/ios/Runner.xcodeproj/project.pbxproj b/templates/basic/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..2021e9f --- /dev/null +++ b/templates/basic/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,614 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807E294A63A400263BE5 /* Frameworks */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.basic; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AE0B7B92F70575B8D7E0D07E /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.basic.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 89B67EB44CE7B6631473024E /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.basic.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 640959BDD8F10B91D80A66BE /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.basic.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.basic; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.basic; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/templates/basic/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/templates/basic/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/templates/basic/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/templates/basic/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/templates/basic/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/templates/basic/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/templates/basic/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/templates/basic/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/templates/basic/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/templates/basic/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/templates/basic/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..87131a0 --- /dev/null +++ b/templates/basic/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/basic/ios/Runner.xcworkspace/contents.xcworkspacedata b/templates/basic/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/templates/basic/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/templates/basic/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/templates/basic/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/templates/basic/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/templates/basic/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/templates/basic/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/templates/basic/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/templates/basic/ios/Runner/AppDelegate.swift b/templates/basic/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/templates/basic/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..b55e5ae Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..8c3cffd Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..a832c2f Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..fb53e1a Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..a6dfd4f Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..1ce6b69 Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..361c941 Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..a832c2f Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..e8b40dc Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..d76aa69 Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..cfa21e0 Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..e011b67 Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..576a3fb Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..8e70167 Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..d76aa69 Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..69ae738 Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..8b92f00 Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..ae83113 Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c013253 Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8d56b95 Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..93f03f4 Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/templates/basic/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/templates/basic/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/templates/basic/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/templates/basic/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/templates/basic/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/templates/basic/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/templates/basic/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/templates/basic/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/templates/basic/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/templates/basic/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/templates/basic/ios/Runner/Base.lproj/LaunchScreen.storyboard b/templates/basic/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/templates/basic/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/basic/ios/Runner/Base.lproj/Main.storyboard b/templates/basic/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/templates/basic/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/basic/ios/Runner/Info.plist b/templates/basic/ios/Runner/Info.plist new file mode 100644 index 0000000..da8aabd --- /dev/null +++ b/templates/basic/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Basic + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + basic + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/templates/basic/ios/Runner/Runner-Bridging-Header.h b/templates/basic/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/templates/basic/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/templates/basic/ios/RunnerTests/RunnerTests.swift b/templates/basic/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/templates/basic/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/templates/basic/lib/app_lifecycle/app_lifecycle.dart b/templates/basic/lib/app_lifecycle/app_lifecycle.dart new file mode 100644 index 0000000..641d40f --- /dev/null +++ b/templates/basic/lib/app_lifecycle/app_lifecycle.dart @@ -0,0 +1,67 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:logging/logging.dart'; +import 'package:provider/provider.dart'; + +typedef AppLifecycleStateNotifier = ValueNotifier; + +// TODO(filiph): use the new AppLifecycleListener class, e.g. +// https://kazlauskas.dev/flutter-app-lifecycle-listener-overview/ +class AppLifecycleObserver extends StatefulWidget { + final Widget child; + + const AppLifecycleObserver({required this.child, super.key}); + + @override + State createState() => _AppLifecycleObserverState(); +} + +class _AppLifecycleObserverState extends State + with WidgetsBindingObserver { + static final _log = Logger('AppLifecycleObserver'); + + final ValueNotifier lifecycleListenable = + ValueNotifier(AppLifecycleState.inactive); + + @override + Widget build(BuildContext context) { + // Using InheritedProvider because we don't want to use Consumer + // or context.watch or anything like that to listen to this. We want + // to manually add listeners. We're interested in the _events_ of lifecycle + // state changes, and not so much in the state itself. (For example, + // we want to stop sound when the app goes into the background, and + // restart sound again when the app goes back into focus. We're not + // rebuilding any widgets.) + // + // Provider, by default, throws when one + // is trying to provide a Listenable (such as ValueNotifier) without using + // something like ValueListenableProvider. InheritedProvider is more + // low-level and doesn't have this problem. + return InheritedProvider.value( + value: lifecycleListenable, + child: widget.child, + ); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + _log.info(() => 'didChangeAppLifecycleState: $state'); + lifecycleListenable.value = state; + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _log.info('Subscribed to app lifecycle updates'); + } +} diff --git a/templates/basic/lib/audio/audio_controller.dart b/templates/basic/lib/audio/audio_controller.dart new file mode 100644 index 0000000..0e05c7c --- /dev/null +++ b/templates/basic/lib/audio/audio_controller.dart @@ -0,0 +1,270 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:collection'; +import 'dart:math'; + +import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:logging/logging.dart'; + +import '../app_lifecycle/app_lifecycle.dart'; +import '../settings/settings.dart'; +import 'songs.dart'; +import 'sounds.dart'; + +/// Allows playing music and sound. A facade to `package:audioplayers`. +class AudioController { + static final _log = Logger('AudioController'); + + final AudioPlayer _musicPlayer; + + /// This is a list of [AudioPlayer] instances which are rotated to play + /// sound effects. + final List _sfxPlayers; + + int _currentSfxPlayer = 0; + + final Queue _playlist; + + final Random _random = Random(); + + SettingsController? _settings; + + ValueNotifier? _lifecycleNotifier; + + /// Creates an instance that plays music and sound. + /// + /// Use [polyphony] to configure the number of sound effects (SFX) that can + /// play at the same time. A [polyphony] of `1` will always only play one + /// sound (a new sound will stop the previous one). See discussion + /// of [_sfxPlayers] to learn why this is the case. + /// + /// Background music does not count into the [polyphony] limit. Music will + /// never be overridden by sound effects because that would be silly. + AudioController({int polyphony = 2}) + : assert(polyphony >= 1), + _musicPlayer = AudioPlayer(playerId: 'musicPlayer'), + _sfxPlayers = Iterable.generate( + polyphony, (i) => AudioPlayer(playerId: 'sfxPlayer#$i')) + .toList(growable: false), + _playlist = Queue.of(List.of(songs)..shuffle()) { + _musicPlayer.onPlayerComplete.listen(_handleSongFinished); + unawaited(_preloadSfx()); + } + + /// Makes sure the audio controller is listening to changes + /// of both the app lifecycle (e.g. suspended app) and to changes + /// of settings (e.g. muted sound). + void attachDependencies(AppLifecycleStateNotifier lifecycleNotifier, + SettingsController settingsController) { + _attachLifecycleNotifier(lifecycleNotifier); + _attachSettings(settingsController); + } + + void dispose() { + _lifecycleNotifier?.removeListener(_handleAppLifecycle); + _stopAllSound(); + _musicPlayer.dispose(); + for (final player in _sfxPlayers) { + player.dispose(); + } + } + + /// Plays a single sound effect, defined by [type]. + /// + /// The controller will ignore this call when the attached settings' + /// [SettingsController.audioOn] is `true` or if its + /// [SettingsController.soundsOn] is `false`. + void playSfx(SfxType type) { + final audioOn = _settings?.audioOn.value ?? false; + if (!audioOn) { + _log.fine(() => 'Ignoring playing sound ($type) because audio is muted.'); + return; + } + final soundsOn = _settings?.soundsOn.value ?? false; + if (!soundsOn) { + _log.fine(() => + 'Ignoring playing sound ($type) because sounds are turned off.'); + return; + } + + _log.fine(() => 'Playing sound: $type'); + final options = soundTypeToFilename(type); + final filename = options[_random.nextInt(options.length)]; + _log.fine(() => '- Chosen filename: $filename'); + + final currentPlayer = _sfxPlayers[_currentSfxPlayer]; + currentPlayer.play(AssetSource('sfx/$filename'), + volume: soundTypeToVolume(type)); + _currentSfxPlayer = (_currentSfxPlayer + 1) % _sfxPlayers.length; + } + + /// Enables the [AudioController] to listen to [AppLifecycleState] events, + /// and therefore do things like stopping playback when the game + /// goes into the background. + void _attachLifecycleNotifier(AppLifecycleStateNotifier lifecycleNotifier) { + _lifecycleNotifier?.removeListener(_handleAppLifecycle); + + lifecycleNotifier.addListener(_handleAppLifecycle); + _lifecycleNotifier = lifecycleNotifier; + } + + /// Enables the [AudioController] to track changes to settings. + /// Namely, when any of [SettingsController.audioOn], + /// [SettingsController.musicOn] or [SettingsController.soundsOn] changes, + /// the audio controller will act accordingly. + void _attachSettings(SettingsController settingsController) { + if (_settings == settingsController) { + // Already attached to this instance. Nothing to do. + return; + } + + // Remove handlers from the old settings controller if present + final oldSettings = _settings; + if (oldSettings != null) { + oldSettings.audioOn.removeListener(_audioOnHandler); + oldSettings.musicOn.removeListener(_musicOnHandler); + oldSettings.soundsOn.removeListener(_soundsOnHandler); + } + + _settings = settingsController; + + // Add handlers to the new settings controller + settingsController.audioOn.addListener(_audioOnHandler); + settingsController.musicOn.addListener(_musicOnHandler); + settingsController.soundsOn.addListener(_soundsOnHandler); + + if (settingsController.audioOn.value && settingsController.musicOn.value) { + if (kIsWeb) { + _log.info('On the web, music can only start after user interaction.'); + } else { + _playCurrentSongInPlaylist(); + } + } + } + + void _audioOnHandler() { + _log.fine('audioOn changed to ${_settings!.audioOn.value}'); + if (_settings!.audioOn.value) { + // All sound just got un-muted. Audio is on. + if (_settings!.musicOn.value) { + _startOrResumeMusic(); + } + } else { + // All sound just got muted. Audio is off. + _stopAllSound(); + } + } + + void _handleAppLifecycle() { + switch (_lifecycleNotifier!.value) { + case AppLifecycleState.paused: + case AppLifecycleState.detached: + case AppLifecycleState.hidden: + _stopAllSound(); + case AppLifecycleState.resumed: + if (_settings!.audioOn.value && _settings!.musicOn.value) { + _startOrResumeMusic(); + } + case AppLifecycleState.inactive: + // No need to react to this state change. + break; + } + } + + void _handleSongFinished(void _) { + _log.info('Last song finished playing.'); + // Move the song that just finished playing to the end of the playlist. + _playlist.addLast(_playlist.removeFirst()); + // Play the song at the beginning of the playlist. + _playCurrentSongInPlaylist(); + } + + void _musicOnHandler() { + if (_settings!.musicOn.value) { + // Music got turned on. + if (_settings!.audioOn.value) { + _startOrResumeMusic(); + } + } else { + // Music got turned off. + _musicPlayer.pause(); + } + } + + Future _playCurrentSongInPlaylist() async { + _log.info(() => 'Playing ${_playlist.first} now.'); + try { + await _musicPlayer.play(AssetSource('music/${_playlist.first.filename}')); + } catch (e) { + _log.severe('Could not play song ${_playlist.first}', e); + } + + // Settings can change while the music player is preparing + // to play a song (i.e. during the `await` above). + // Unfortunately, `audioplayers` has a bug which will ignore calls + // to `pause()` before that await is finished, so we need + // to double check here. + // See issue: https://github.com/bluefireteam/audioplayers/issues/1687 + if (!_settings!.audioOn.value || !_settings!.musicOn.value) { + try { + _log.fine('Settings changed while preparing to play song. ' + 'Pausing music.'); + await _musicPlayer.pause(); + } catch (e) { + _log.severe('Could not pause music player', e); + } + } + } + + /// Preloads all sound effects. + Future _preloadSfx() async { + _log.info('Preloading sound effects'); + // This assumes there is only a limited number of sound effects in the game. + // If there are hundreds of long sound effect files, it's better + // to be more selective when preloading. + await AudioCache.instance.loadAll(SfxType.values + .expand(soundTypeToFilename) + .map((path) => 'sfx/$path') + .toList()); + } + + void _soundsOnHandler() { + for (final player in _sfxPlayers) { + if (player.state == PlayerState.playing) { + player.stop(); + } + } + } + + void _startOrResumeMusic() async { + if (_musicPlayer.source == null) { + _log.info('No music source set. ' + 'Start playing the current song in playlist.'); + await _playCurrentSongInPlaylist(); + return; + } + + _log.info('Resuming paused music.'); + try { + _musicPlayer.resume(); + } catch (e) { + // Sometimes, resuming fails with an "Unexpected" error. + _log.severe("Error resuming music", e); + // Try starting the song from scratch. + _playCurrentSongInPlaylist(); + } + } + + void _stopAllSound() { + _log.info('Stopping all sound'); + _musicPlayer.pause(); + for (final player in _sfxPlayers) { + player.stop(); + } + } +} diff --git a/templates/basic/lib/audio/songs.dart b/templates/basic/lib/audio/songs.dart new file mode 100644 index 0000000..dd542bd --- /dev/null +++ b/templates/basic/lib/audio/songs.dart @@ -0,0 +1,24 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +const Set songs = { + // Filenames with whitespace break package:audioplayers on iOS + // (as of February 2022), so we use no whitespace. + Song('Mr_Smith-Azul.mp3', 'Azul', artist: 'Mr Smith'), + Song('Mr_Smith-Sonorus.mp3', 'Sonorus', artist: 'Mr Smith'), + Song('Mr_Smith-Sunday_Solitude.mp3', 'SundaySolitude', artist: 'Mr Smith'), +}; + +class Song { + final String filename; + + final String name; + + final String? artist; + + const Song(this.filename, this.name, {this.artist}); + + @override + String toString() => 'Song<$filename>'; +} diff --git a/templates/basic/lib/audio/sounds.dart b/templates/basic/lib/audio/sounds.dart new file mode 100644 index 0000000..139ac21 --- /dev/null +++ b/templates/basic/lib/audio/sounds.dart @@ -0,0 +1,71 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +List soundTypeToFilename(SfxType type) { + switch (type) { + case SfxType.huhsh: + return const [ + 'hash1.mp3', + 'hash2.mp3', + 'hash3.mp3', + ]; + case SfxType.wssh: + return const [ + 'wssh1.mp3', + 'wssh2.mp3', + 'dsht1.mp3', + 'ws1.mp3', + 'spsh1.mp3', + 'hh1.mp3', + 'hh2.mp3', + 'kss1.mp3', + ]; + case SfxType.buttonTap: + return const [ + 'k1.mp3', + 'k2.mp3', + 'p1.mp3', + 'p2.mp3', + ]; + case SfxType.congrats: + return const [ + 'yay1.mp3', + 'wehee1.mp3', + 'oo1.mp3', + ]; + case SfxType.erase: + return const [ + 'fwfwfwfwfw1.mp3', + 'fwfwfwfw1.mp3', + ]; + case SfxType.swishSwish: + return const [ + 'swishswish1.mp3', + ]; + } +} + +/// Allows control over loudness of different SFX types. +double soundTypeToVolume(SfxType type) { + switch (type) { + case SfxType.huhsh: + return 0.4; + case SfxType.wssh: + return 0.2; + case SfxType.buttonTap: + case SfxType.congrats: + case SfxType.erase: + case SfxType.swishSwish: + return 1.0; + } +} + +enum SfxType { + huhsh, + wssh, + buttonTap, + congrats, + erase, + swishSwish, +} diff --git a/templates/basic/lib/game_internals/level_state.dart b/templates/basic/lib/game_internals/level_state.dart new file mode 100644 index 0000000..0d765f0 --- /dev/null +++ b/templates/basic/lib/game_internals/level_state.dart @@ -0,0 +1,32 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/foundation.dart'; + +/// An extremely silly example of a game state. +/// +/// Tracks only a single variable, [progress], and calls [onWin] when +/// the value of [progress] reaches [goal]. +class LevelState extends ChangeNotifier { + final VoidCallback onWin; + + final int goal; + + LevelState({required this.onWin, this.goal = 100}); + + int _progress = 0; + + int get progress => _progress; + + void setProgress(int value) { + _progress = value; + notifyListeners(); + } + + void evaluate() { + if (_progress >= goal) { + onWin(); + } + } +} diff --git a/templates/basic/lib/game_internals/score.dart b/templates/basic/lib/game_internals/score.dart new file mode 100644 index 0000000..ec9a1d6 --- /dev/null +++ b/templates/basic/lib/game_internals/score.dart @@ -0,0 +1,45 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Encapsulates a score and the arithmetic to compute it. +class Score { + final int score; + + final Duration duration; + + final int level; + + factory Score(int level, int difficulty, Duration duration) { + // The higher the difficulty, the higher the score. + var score = difficulty; + // The lower the time to beat the level, the higher the score. + score *= 10000 ~/ (duration.inSeconds.abs() + 1); + return Score._(score, duration, level); + } + + const Score._(this.score, this.duration, this.level); + + String get formattedTime { + final buf = StringBuffer(); + if (duration.inHours > 0) { + buf.write('${duration.inHours}'); + buf.write(':'); + } + final minutes = duration.inMinutes % Duration.minutesPerHour; + if (minutes > 9) { + buf.write('$minutes'); + } else { + buf.write('0'); + buf.write('$minutes'); + } + buf.write(':'); + buf.write((duration.inSeconds % Duration.secondsPerMinute) + .toString() + .padLeft(2, '0')); + return buf.toString(); + } + + @override + String toString() => 'Score<$score,$formattedTime,$level>'; +} diff --git a/templates/basic/lib/level_selection/level_selection_screen.dart b/templates/basic/lib/level_selection/level_selection_screen.dart new file mode 100644 index 0000000..511527b --- /dev/null +++ b/templates/basic/lib/level_selection/level_selection_screen.dart @@ -0,0 +1,72 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../audio/audio_controller.dart'; +import '../audio/sounds.dart'; +import '../player_progress/player_progress.dart'; +import '../style/my_button.dart'; +import '../style/palette.dart'; +import '../style/responsive_screen.dart'; +import 'levels.dart'; + +class LevelSelectionScreen extends StatelessWidget { + const LevelSelectionScreen({super.key}); + + @override + Widget build(BuildContext context) { + final palette = context.watch(); + final playerProgress = context.watch(); + + return Scaffold( + backgroundColor: palette.backgroundLevelSelection, + body: ResponsiveScreen( + squarishMainArea: Column( + children: [ + const Padding( + padding: EdgeInsets.all(16), + child: Center( + child: Text( + 'Select level', + style: + TextStyle(fontFamily: 'Permanent Marker', fontSize: 30), + ), + ), + ), + const SizedBox(height: 50), + Expanded( + child: ListView( + children: [ + for (final level in gameLevels) + ListTile( + enabled: playerProgress.highestLevelReached >= + level.number - 1, + onTap: () { + final audioController = context.read(); + audioController.playSfx(SfxType.buttonTap); + + GoRouter.of(context) + .go('/play/session/${level.number}'); + }, + leading: Text(level.number.toString()), + title: Text('Level #${level.number}'), + ) + ], + ), + ), + ], + ), + rectangularMenuArea: MyButton( + onPressed: () { + GoRouter.of(context).go('/'); + }, + child: const Text('Back'), + ), + ), + ); + } +} diff --git a/templates/basic/lib/level_selection/levels.dart b/templates/basic/lib/level_selection/levels.dart new file mode 100644 index 0000000..c8ef578 --- /dev/null +++ b/templates/basic/lib/level_selection/levels.dart @@ -0,0 +1,49 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +const gameLevels = [ + GameLevel( + number: 1, + difficulty: 5, + // TODO: When ready, change these achievement IDs. + // You configure this in App Store Connect. + achievementIdIOS: 'first_win', + // You get this string when you configure an achievement in Play Console. + achievementIdAndroid: 'NhkIwB69ejkMAOOLDb', + ), + GameLevel( + number: 2, + difficulty: 42, + ), + GameLevel( + number: 3, + difficulty: 100, + achievementIdIOS: 'finished', + achievementIdAndroid: 'CdfIhE96aspNWLGSQg', + ), +]; + +class GameLevel { + final int number; + + final int difficulty; + + /// The achievement to unlock when the level is finished, if any. + final String? achievementIdIOS; + + final String? achievementIdAndroid; + + bool get awardsAchievement => achievementIdAndroid != null; + + const GameLevel({ + required this.number, + required this.difficulty, + this.achievementIdIOS, + this.achievementIdAndroid, + }) : assert( + (achievementIdAndroid != null && achievementIdIOS != null) || + (achievementIdAndroid == null && achievementIdIOS == null), + 'Either both iOS and Android achievement ID must be provided, ' + 'or none'); +} diff --git a/templates/basic/lib/main.dart b/templates/basic/lib/main.dart new file mode 100644 index 0000000..9c2be0b --- /dev/null +++ b/templates/basic/lib/main.dart @@ -0,0 +1,107 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:developer' as dev; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:logging/logging.dart'; +import 'package:provider/provider.dart'; + +import 'app_lifecycle/app_lifecycle.dart'; +import 'audio/audio_controller.dart'; +import 'player_progress/player_progress.dart'; +import 'router.dart'; +import 'settings/settings.dart'; +import 'style/palette.dart'; + +void main() async { + // Basic logging setup. + Logger.root.level = kDebugMode ? Level.FINE : Level.INFO; + Logger.root.onRecord.listen((record) { + dev.log( + record.message, + time: record.time, + level: record.level.value, + name: record.loggerName, + ); + }); + + WidgetsFlutterBinding.ensureInitialized(); + // Put game into full screen mode on mobile devices. + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + // Lock the game to portrait mode on mobile devices. + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return AppLifecycleObserver( + child: MultiProvider( + // This is where you add objects that you want to have available + // throughout your game. + // + // Every widget in the game can access these objects by calling + // `context.watch()` or `context.read()`. + // See `lib/main_menu/main_menu_screen.dart` for example usage. + providers: [ + Provider(create: (context) => SettingsController()), + Provider(create: (context) => Palette()), + ChangeNotifierProvider(create: (context) => PlayerProgress()), + // Set up audio. + ProxyProvider2( + create: (context) => AudioController(), + update: (context, lifecycleNotifier, settings, audio) { + audio!.attachDependencies(lifecycleNotifier, settings); + return audio; + }, + dispose: (context, audio) => audio.dispose(), + // Ensures that music starts immediately. + lazy: false, + ), + ], + child: Builder(builder: (context) { + final palette = context.watch(); + + return MaterialApp.router( + title: 'My Flutter Game', + theme: ThemeData.from( + colorScheme: ColorScheme.fromSeed( + seedColor: palette.darkPen, + background: palette.backgroundMain, + ), + textTheme: TextTheme( + bodyMedium: TextStyle(color: palette.ink), + ), + useMaterial3: true, + ).copyWith( + // Make buttons more fun. + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + textStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ), + ), + ), + routeInformationProvider: router.routeInformationProvider, + routeInformationParser: router.routeInformationParser, + routerDelegate: router.routerDelegate, + ); + }), + ), + ); + } +} diff --git a/templates/basic/lib/main_menu/main_menu_screen.dart b/templates/basic/lib/main_menu/main_menu_screen.dart new file mode 100644 index 0000000..4cb5f52 --- /dev/null +++ b/templates/basic/lib/main_menu/main_menu_screen.dart @@ -0,0 +1,80 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../audio/audio_controller.dart'; +import '../audio/sounds.dart'; +import '../settings/settings.dart'; +import '../style/my_button.dart'; +import '../style/palette.dart'; +import '../style/responsive_screen.dart'; + +class MainMenuScreen extends StatelessWidget { + const MainMenuScreen({super.key}); + + @override + Widget build(BuildContext context) { + final palette = context.watch(); + final settingsController = context.watch(); + final audioController = context.watch(); + + return Scaffold( + backgroundColor: palette.backgroundMain, + body: ResponsiveScreen( + squarishMainArea: Center( + child: Transform.rotate( + angle: -0.1, + child: const Text( + 'Flutter Game Template!', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Permanent Marker', + fontSize: 55, + height: 1, + ), + ), + ), + ), + rectangularMenuArea: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + MyButton( + onPressed: () { + audioController.playSfx(SfxType.buttonTap); + GoRouter.of(context).go('/play'); + }, + child: const Text('Play'), + ), + _gap, + MyButton( + onPressed: () => GoRouter.of(context).push('/settings'), + child: const Text('Settings'), + ), + _gap, + Padding( + padding: const EdgeInsets.only(top: 32), + child: ValueListenableBuilder( + valueListenable: settingsController.audioOn, + builder: (context, audioOn, child) { + return IconButton( + onPressed: () => settingsController.toggleAudioOn(), + icon: Icon(audioOn ? Icons.volume_up : Icons.volume_off), + ); + }, + ), + ), + _gap, + const Text('Music by Mr Smith'), + _gap, + ], + ), + ), + ); + } + + static const _gap = SizedBox(height: 10); +} diff --git a/templates/basic/lib/play_session/game_widget.dart b/templates/basic/lib/play_session/game_widget.dart new file mode 100644 index 0000000..dea9318 --- /dev/null +++ b/templates/basic/lib/play_session/game_widget.dart @@ -0,0 +1,39 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../audio/audio_controller.dart'; +import '../audio/sounds.dart'; +import '../game_internals/level_state.dart'; +import '../level_selection/levels.dart'; + +/// This widget defines the game UI itself, without things like the settings +/// button or the back button. +class GameWidget extends StatelessWidget { + const GameWidget({super.key}); + + @override + Widget build(BuildContext context) { + final level = context.watch(); + final levelState = context.watch(); + + return Column( + children: [ + Text('Drag the slider to ${level.difficulty}% or above!'), + Slider( + label: 'Level Progress', + autofocus: true, + value: levelState.progress / 100, + onChanged: (value) => levelState.setProgress((value * 100).round()), + onChangeEnd: (value) { + context.read().playSfx(SfxType.wssh); + levelState.evaluate(); + }, + ), + ], + ); + } +} diff --git a/templates/basic/lib/play_session/play_session_screen.dart b/templates/basic/lib/play_session/play_session_screen.dart new file mode 100644 index 0000000..d52ed2d --- /dev/null +++ b/templates/basic/lib/play_session/play_session_screen.dart @@ -0,0 +1,160 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart' hide Level; +import 'package:provider/provider.dart'; + +import '../audio/audio_controller.dart'; +import '../audio/sounds.dart'; +import '../game_internals/level_state.dart'; +import '../game_internals/score.dart'; +import '../level_selection/levels.dart'; +import '../player_progress/player_progress.dart'; +import '../style/confetti.dart'; +import '../style/my_button.dart'; +import '../style/palette.dart'; +import 'game_widget.dart'; + +/// This widget defines the entirety of the screen that the player sees when +/// they are playing a level. +/// +/// It is a stateful widget because it manages some state of its own, +/// such as whether the game is in a "celebration" state. +class PlaySessionScreen extends StatefulWidget { + final GameLevel level; + + const PlaySessionScreen(this.level, {super.key}); + + @override + State createState() => _PlaySessionScreenState(); +} + +class _PlaySessionScreenState extends State { + static final _log = Logger('PlaySessionScreen'); + + static const _celebrationDuration = Duration(milliseconds: 2000); + + static const _preCelebrationDuration = Duration(milliseconds: 500); + + bool _duringCelebration = false; + + late DateTime _startOfPlay; + + @override + void initState() { + super.initState(); + + _startOfPlay = DateTime.now(); + } + + @override + Widget build(BuildContext context) { + final palette = context.watch(); + + return MultiProvider( + providers: [ + Provider.value(value: widget.level), + // Create and provide the [LevelState] object that will be used + // by widgets below this one in the widget tree. + ChangeNotifierProvider( + create: (context) => LevelState( + goal: widget.level.difficulty, + onWin: _playerWon, + ), + ), + ], + child: IgnorePointer( + // Ignore all input during the celebration animation. + ignoring: _duringCelebration, + child: Scaffold( + backgroundColor: palette.backgroundPlaySession, + // The stack is how you layer widgets on top of each other. + // Here, it is used to overlay the winning confetti animation on top + // of the game. + body: Stack( + children: [ + // This is the main layout of the play session screen, + // with a settings button on top, the actual play area + // in the middle, and a back button at the bottom. + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Align( + alignment: Alignment.centerRight, + child: InkResponse( + onTap: () => GoRouter.of(context).push('/settings'), + child: Image.asset( + 'assets/images/settings.png', + semanticLabel: 'Settings', + ), + ), + ), + const Spacer(), + Expanded( + // The actual UI of the game. + child: GameWidget(), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(8.0), + child: MyButton( + onPressed: () => GoRouter.of(context).go('/play'), + child: const Text('Back'), + ), + ), + ], + ), + // This is the confetti animation that is overlaid on top of the + // game when the player wins. + SizedBox.expand( + child: Visibility( + visible: _duringCelebration, + child: IgnorePointer( + child: Confetti( + isStopped: !_duringCelebration, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + Future _playerWon() async { + _log.info('Level ${widget.level.number} won'); + + final score = Score( + widget.level.number, + widget.level.difficulty, + DateTime.now().difference(_startOfPlay), + ); + + final playerProgress = context.read(); + playerProgress.setLevelReached(widget.level.number); + + // Let the player see the game just after winning for a bit. + await Future.delayed(_preCelebrationDuration); + if (!mounted) return; + + setState(() { + _duringCelebration = true; + }); + + final audioController = context.read(); + audioController.playSfx(SfxType.congrats); + + /// Give the player some time to see the celebration animation. + await Future.delayed(_celebrationDuration); + if (!mounted) return; + + GoRouter.of(context).go('/play/won', extra: {'score': score}); + } +} diff --git a/templates/basic/lib/player_progress/persistence/local_storage_player_progress_persistence.dart b/templates/basic/lib/player_progress/persistence/local_storage_player_progress_persistence.dart new file mode 100644 index 0000000..66c0d35 --- /dev/null +++ b/templates/basic/lib/player_progress/persistence/local_storage_player_progress_persistence.dart @@ -0,0 +1,26 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:shared_preferences/shared_preferences.dart'; + +import 'player_progress_persistence.dart'; + +/// An implementation of [PlayerProgressPersistence] that uses +/// `package:shared_preferences`. +class LocalStoragePlayerProgressPersistence extends PlayerProgressPersistence { + final Future instanceFuture = + SharedPreferences.getInstance(); + + @override + Future getHighestLevelReached() async { + final prefs = await instanceFuture; + return prefs.getInt('highestLevelReached') ?? 0; + } + + @override + Future saveHighestLevelReached(int level) async { + final prefs = await instanceFuture; + await prefs.setInt('highestLevelReached', level); + } +} diff --git a/templates/basic/lib/player_progress/persistence/memory_player_progress_persistence.dart b/templates/basic/lib/player_progress/persistence/memory_player_progress_persistence.dart new file mode 100644 index 0000000..0913705 --- /dev/null +++ b/templates/basic/lib/player_progress/persistence/memory_player_progress_persistence.dart @@ -0,0 +1,23 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'player_progress_persistence.dart'; + +/// An in-memory implementation of [PlayerProgressPersistence]. +/// Useful for testing. +class MemoryOnlyPlayerProgressPersistence implements PlayerProgressPersistence { + int level = 0; + + @override + Future getHighestLevelReached() async { + await Future.delayed(const Duration(milliseconds: 500)); + return level; + } + + @override + Future saveHighestLevelReached(int level) async { + await Future.delayed(const Duration(milliseconds: 500)); + this.level = level; + } +} diff --git a/templates/basic/lib/player_progress/persistence/player_progress_persistence.dart b/templates/basic/lib/player_progress/persistence/player_progress_persistence.dart new file mode 100644 index 0000000..123027a --- /dev/null +++ b/templates/basic/lib/player_progress/persistence/player_progress_persistence.dart @@ -0,0 +1,13 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// An interface of persistence stores for the player's progress. +/// +/// Implementations can range from simple in-memory storage through +/// local preferences to cloud saves. +abstract class PlayerProgressPersistence { + Future getHighestLevelReached(); + + Future saveHighestLevelReached(int level); +} diff --git a/templates/basic/lib/player_progress/player_progress.dart b/templates/basic/lib/player_progress/player_progress.dart new file mode 100644 index 0000000..868d115 --- /dev/null +++ b/templates/basic/lib/player_progress/player_progress.dart @@ -0,0 +1,64 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import 'persistence/local_storage_player_progress_persistence.dart'; +import 'persistence/player_progress_persistence.dart'; + +/// Encapsulates the player's progress. +class PlayerProgress extends ChangeNotifier { + static const maxHighestScoresPerPlayer = 10; + + /// By default, settings are persisted using + /// [LocalStoragePlayerProgressPersistence] (i.e. NSUserDefaults on iOS, + /// SharedPreferences on Android or local storage on the web). + final PlayerProgressPersistence _store; + + int _highestLevelReached = 0; + + /// Creates an instance of [PlayerProgress] backed by an injected + /// persistence [store]. + PlayerProgress({PlayerProgressPersistence? store}) + : _store = store ?? LocalStoragePlayerProgressPersistence() { + _getLatestFromStore(); + } + + /// The highest level that the player has reached so far. + int get highestLevelReached => _highestLevelReached; + + /// Resets the player's progress so it's like if they just started + /// playing the game for the first time. + void reset() { + _highestLevelReached = 0; + notifyListeners(); + _store.saveHighestLevelReached(_highestLevelReached); + } + + /// Registers [level] as reached. + /// + /// If this is higher than [highestLevelReached], it will update that + /// value and save it to the injected persistence store. + void setLevelReached(int level) { + if (level > _highestLevelReached) { + _highestLevelReached = level; + notifyListeners(); + + unawaited(_store.saveHighestLevelReached(level)); + } + } + + /// Fetches the latest data from the backing persistence store. + Future _getLatestFromStore() async { + final level = await _store.getHighestLevelReached(); + if (level > _highestLevelReached) { + _highestLevelReached = level; + notifyListeners(); + } else if (level < _highestLevelReached) { + await _store.saveHighestLevelReached(_highestLevelReached); + } + } +} diff --git a/templates/basic/lib/router.dart b/templates/basic/lib/router.dart new file mode 100644 index 0000000..3e23e87 --- /dev/null +++ b/templates/basic/lib/router.dart @@ -0,0 +1,88 @@ +// Copyright 2023, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import 'game_internals/score.dart'; +import 'level_selection/level_selection_screen.dart'; +import 'level_selection/levels.dart'; +import 'main_menu/main_menu_screen.dart'; +import 'play_session/play_session_screen.dart'; +import 'settings/settings_screen.dart'; +import 'style/my_transition.dart'; +import 'style/palette.dart'; +import 'win_game/win_game_screen.dart'; + +/// The router describes the game's navigational hierarchy, from the main +/// screen through settings screens all the way to each individual level. +final router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const MainMenuScreen(key: Key('main menu')), + routes: [ + GoRoute( + path: 'play', + pageBuilder: (context, state) => buildMyTransition( + key: ValueKey('play'), + color: context.watch().backgroundLevelSelection, + child: const LevelSelectionScreen( + key: Key('level selection'), + ), + ), + routes: [ + GoRoute( + path: 'session/:level', + pageBuilder: (context, state) { + final levelNumber = int.parse(state.pathParameters['level']!); + final level = + gameLevels.singleWhere((e) => e.number == levelNumber); + return buildMyTransition( + key: ValueKey('level'), + color: context.watch().backgroundPlaySession, + child: PlaySessionScreen( + level, + key: const Key('play session'), + ), + ); + }, + ), + GoRoute( + path: 'won', + redirect: (context, state) { + if (state.extra == null) { + // Trying to navigate to a win screen without any data. + // Possibly by using the browser's back button. + return '/'; + } + + // Otherwise, do not redirect. + return null; + }, + pageBuilder: (context, state) { + final map = state.extra! as Map; + final score = map['score'] as Score; + + return buildMyTransition( + key: ValueKey('won'), + color: context.watch().backgroundPlaySession, + child: WinGameScreen( + score: score, + key: const Key('win game'), + ), + ); + }, + ) + ]), + GoRoute( + path: 'settings', + builder: (context, state) => + const SettingsScreen(key: Key('settings')), + ), + ], + ), + ], +); diff --git a/templates/basic/lib/settings/custom_name_dialog.dart b/templates/basic/lib/settings/custom_name_dialog.dart new file mode 100644 index 0000000..59dbcbb --- /dev/null +++ b/templates/basic/lib/settings/custom_name_dialog.dart @@ -0,0 +1,76 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +import 'settings.dart'; + +void showCustomNameDialog(BuildContext context) { + showGeneralDialog( + context: context, + pageBuilder: (context, animation, secondaryAnimation) => + CustomNameDialog(animation: animation)); +} + +class CustomNameDialog extends StatefulWidget { + final Animation animation; + + const CustomNameDialog({required this.animation, super.key}); + + @override + State createState() => _CustomNameDialogState(); +} + +class _CustomNameDialogState extends State { + final TextEditingController _controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + return ScaleTransition( + scale: CurvedAnimation( + parent: widget.animation, + curve: Curves.easeOutCubic, + ), + child: SimpleDialog( + title: const Text('Change name'), + children: [ + TextField( + controller: _controller, + autofocus: true, + maxLength: 12, + maxLengthEnforcement: MaxLengthEnforcement.enforced, + textAlign: TextAlign.center, + textCapitalization: TextCapitalization.words, + textInputAction: TextInputAction.done, + onChanged: (value) { + context.read().setPlayerName(value); + }, + onSubmitted: (value) { + // Player tapped 'Submit'/'Done' on their keyboard. + Navigator.pop(context); + }, + ), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + _controller.text = context.read().playerName.value; + } +} diff --git a/templates/basic/lib/settings/persistence/local_storage_settings_persistence.dart b/templates/basic/lib/settings/persistence/local_storage_settings_persistence.dart new file mode 100644 index 0000000..41c56aa --- /dev/null +++ b/templates/basic/lib/settings/persistence/local_storage_settings_persistence.dart @@ -0,0 +1,62 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:shared_preferences/shared_preferences.dart'; + +import 'settings_persistence.dart'; + +/// An implementation of [SettingsPersistence] that uses +/// `package:shared_preferences`. +class LocalStorageSettingsPersistence extends SettingsPersistence { + final Future instanceFuture = + SharedPreferences.getInstance(); + + @override + Future getAudioOn({required bool defaultValue}) async { + final prefs = await instanceFuture; + return prefs.getBool('mute') ?? defaultValue; + } + + @override + Future getMusicOn({required bool defaultValue}) async { + final prefs = await instanceFuture; + return prefs.getBool('musicOn') ?? defaultValue; + } + + @override + Future getPlayerName() async { + final prefs = await instanceFuture; + return prefs.getString('playerName') ?? 'Player'; + } + + @override + Future getSoundsOn({required bool defaultValue}) async { + final prefs = await instanceFuture; + return prefs.getBool('soundsOn') ?? defaultValue; + } + + @override + Future saveAudioOn(bool value) async { + final prefs = await instanceFuture; + await prefs.setBool('mute', value); + } + + @override + Future saveMusicOn(bool value) async { + final prefs = await instanceFuture; + await prefs.setBool('musicOn', value); + } + + @override + Future savePlayerName(String value) async { + final prefs = await instanceFuture; + await prefs.setString('playerName', value); + } + + @override + Future saveSoundsOn(bool value) async { + final prefs = await instanceFuture; + await prefs.setBool('soundsOn', value); + } +} diff --git a/templates/basic/lib/settings/persistence/memory_settings_persistence.dart b/templates/basic/lib/settings/persistence/memory_settings_persistence.dart new file mode 100644 index 0000000..14a9df7 --- /dev/null +++ b/templates/basic/lib/settings/persistence/memory_settings_persistence.dart @@ -0,0 +1,41 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'settings_persistence.dart'; + +/// An in-memory implementation of [SettingsPersistence]. +/// Useful for testing. +class MemoryOnlySettingsPersistence implements SettingsPersistence { + bool musicOn = true; + + bool soundsOn = true; + + bool audioOn = true; + + String playerName = 'Player'; + + @override + Future getAudioOn({required bool defaultValue}) async => audioOn; + + @override + Future getMusicOn({required bool defaultValue}) async => musicOn; + + @override + Future getPlayerName() async => playerName; + + @override + Future getSoundsOn({required bool defaultValue}) async => soundsOn; + + @override + Future saveAudioOn(bool value) async => audioOn = value; + + @override + Future saveMusicOn(bool value) async => musicOn = value; + + @override + Future savePlayerName(String value) async => playerName = value; + + @override + Future saveSoundsOn(bool value) async => soundsOn = value; +} diff --git a/templates/basic/lib/settings/persistence/settings_persistence.dart b/templates/basic/lib/settings/persistence/settings_persistence.dart new file mode 100644 index 0000000..af93abf --- /dev/null +++ b/templates/basic/lib/settings/persistence/settings_persistence.dart @@ -0,0 +1,25 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// An interface of persistence stores for settings. +/// +/// Implementations can range from simple in-memory storage through +/// local preferences to cloud-based solutions. +abstract class SettingsPersistence { + Future getAudioOn({required bool defaultValue}); + + Future getMusicOn({required bool defaultValue}); + + Future getPlayerName(); + + Future getSoundsOn({required bool defaultValue}); + + Future saveAudioOn(bool value); + + Future saveMusicOn(bool value); + + Future savePlayerName(String value); + + Future saveSoundsOn(bool value); +} diff --git a/templates/basic/lib/settings/settings.dart b/templates/basic/lib/settings/settings.dart new file mode 100644 index 0000000..4b9f93d --- /dev/null +++ b/templates/basic/lib/settings/settings.dart @@ -0,0 +1,91 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; + +import 'persistence/local_storage_settings_persistence.dart'; +import 'persistence/settings_persistence.dart'; + +/// An class that holds settings like [playerName] or [musicOn], +/// and saves them to an injected persistence store. +class SettingsController { + static final _log = Logger('SettingsController'); + + /// The persistence store that is used to save settings. + final SettingsPersistence _store; + + /// Whether or not the audio is on at all. This overrides both music + /// and sounds (sfx). + /// + /// This is an important feature especially on mobile, where players + /// expect to be able to quickly mute all the audio. Having this as + /// a separate flag (as opposed to some kind of {off, sound, everything} + /// enum) means that the player will not lose their [soundsOn] and + /// [musicOn] preferences when they temporarily mute the game. + ValueNotifier audioOn = ValueNotifier(true); + + /// The player's name. Used for things like high score lists. + ValueNotifier playerName = ValueNotifier('Player'); + + /// Whether or not the sound effects (sfx) are on. + ValueNotifier soundsOn = ValueNotifier(true); + + /// Whether or not the music is on. + ValueNotifier musicOn = ValueNotifier(true); + + /// Creates a new instance of [SettingsController] backed by [store]. + /// + /// By default, settings are persisted using [LocalStorageSettingsPersistence] + /// (i.e. NSUserDefaults on iOS, SharedPreferences on Android or + /// local storage on the web). + SettingsController({SettingsPersistence? store}) + : _store = store ?? LocalStorageSettingsPersistence() { + _loadStateFromPersistence(); + } + + void setPlayerName(String name) { + playerName.value = name; + _store.savePlayerName(playerName.value); + } + + void toggleAudioOn() { + audioOn.value = !audioOn.value; + _store.saveAudioOn(audioOn.value); + } + + void toggleMusicOn() { + musicOn.value = !musicOn.value; + _store.saveMusicOn(musicOn.value); + } + + void toggleSoundsOn() { + soundsOn.value = !soundsOn.value; + _store.saveSoundsOn(soundsOn.value); + } + + /// Asynchronously loads values from the injected persistence store. + Future _loadStateFromPersistence() async { + final loadedValues = await Future.wait([ + _store.getAudioOn(defaultValue: true).then((value) { + if (kIsWeb) { + // On the web, sound can only start after user interaction, so + // we start muted there on every game start. + return audioOn.value = false; + } + // On other platforms, we can use the persisted value. + return audioOn.value = value; + }), + _store + .getSoundsOn(defaultValue: true) + .then((value) => soundsOn.value = value), + _store + .getMusicOn(defaultValue: true) + .then((value) => musicOn.value = value), + _store.getPlayerName().then((value) => playerName.value = value), + ]); + + _log.fine(() => 'Loaded settings: $loadedValues'); + } +} diff --git a/templates/basic/lib/settings/settings_screen.dart b/templates/basic/lib/settings/settings_screen.dart new file mode 100644 index 0000000..c62142a --- /dev/null +++ b/templates/basic/lib/settings/settings_screen.dart @@ -0,0 +1,164 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../player_progress/player_progress.dart'; +import '../style/my_button.dart'; +import '../style/palette.dart'; +import '../style/responsive_screen.dart'; +import 'custom_name_dialog.dart'; +import 'settings.dart'; + +class SettingsScreen extends StatelessWidget { + const SettingsScreen({super.key}); + + static const _gap = SizedBox(height: 60); + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + final palette = context.watch(); + + return Scaffold( + backgroundColor: palette.backgroundSettings, + body: ResponsiveScreen( + squarishMainArea: ListView( + children: [ + _gap, + const Text( + 'Settings', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Permanent Marker', + fontSize: 55, + height: 1, + ), + ), + _gap, + const _NameChangeLine( + 'Name', + ), + ValueListenableBuilder( + valueListenable: settings.soundsOn, + builder: (context, soundsOn, child) => _SettingsLine( + 'Sound FX', + Icon(soundsOn ? Icons.graphic_eq : Icons.volume_off), + onSelected: () => settings.toggleSoundsOn(), + ), + ), + ValueListenableBuilder( + valueListenable: settings.musicOn, + builder: (context, musicOn, child) => _SettingsLine( + 'Music', + Icon(musicOn ? Icons.music_note : Icons.music_off), + onSelected: () => settings.toggleMusicOn(), + ), + ), + _SettingsLine( + 'Reset progress', + const Icon(Icons.delete), + onSelected: () { + context.read().reset(); + + final messenger = ScaffoldMessenger.of(context); + messenger.showSnackBar( + const SnackBar( + content: Text('Player progress has been reset.')), + ); + }, + ), + _gap, + ], + ), + rectangularMenuArea: MyButton( + onPressed: () { + GoRouter.of(context).pop(); + }, + child: const Text('Back'), + ), + ), + ); + } +} + +class _NameChangeLine extends StatelessWidget { + final String title; + + const _NameChangeLine(this.title); + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + + return InkResponse( + highlightShape: BoxShape.rectangle, + onTap: () => showCustomNameDialog(context), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(title, + style: const TextStyle( + fontFamily: 'Permanent Marker', + fontSize: 30, + )), + const Spacer(), + ValueListenableBuilder( + valueListenable: settings.playerName, + builder: (context, name, child) => Text( + '‘$name’', + style: const TextStyle( + fontFamily: 'Permanent Marker', + fontSize: 30, + ), + ), + ), + ], + ), + ), + ); + } +} + +class _SettingsLine extends StatelessWidget { + final String title; + + final Widget icon; + + final VoidCallback? onSelected; + + const _SettingsLine(this.title, this.icon, {this.onSelected}); + + @override + Widget build(BuildContext context) { + return InkResponse( + highlightShape: BoxShape.rectangle, + onTap: onSelected, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontFamily: 'Permanent Marker', + fontSize: 30, + ), + ), + ), + icon, + ], + ), + ), + ); + } +} diff --git a/templates/basic/lib/style/confetti.dart b/templates/basic/lib/style/confetti.dart new file mode 100644 index 0000000..b835224 --- /dev/null +++ b/templates/basic/lib/style/confetti.dart @@ -0,0 +1,234 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:collection'; +import 'dart:math'; + +import 'package:flutter/widgets.dart'; + +/// Shows a confetti (celebratory) animation: paper snippings falling down. +/// +/// The widget fills the available space (like [SizedBox.expand] would). +/// +/// When [isStopped] is `true`, the animation will not run. This is useful +/// when the widget is not visible yet, for example. Provide [colors] +/// to make the animation look good in context. +/// +/// This is a partial port of this CodePen by Hemn Chawroka: +/// https://codepen.io/iprodev/pen/azpWBr +class Confetti extends StatefulWidget { + static const _defaultColors = [ + Color(0xffd10841), + Color(0xff1d75fb), + Color(0xff0050bc), + Color(0xffa2dcc7), + ]; + + final bool isStopped; + + final List colors; + + const Confetti({ + this.colors = _defaultColors, + this.isStopped = false, + super.key, + }); + + @override + State createState() => _ConfettiState(); +} + +class ConfettiPainter extends CustomPainter { + final defaultPaint = Paint(); + + final int snippingsCount = 200; + + late final List<_PaperSnipping> _snippings; + + Size? _size; + + DateTime _lastTime = DateTime.now(); + + final UnmodifiableListView colors; + + ConfettiPainter( + {required Listenable animation, required Iterable colors}) + : colors = UnmodifiableListView(colors), + super(repaint: animation); + + @override + void paint(Canvas canvas, Size size) { + if (_size == null) { + // First time we have a size. + _snippings = List.generate( + snippingsCount, + (i) => _PaperSnipping( + frontColor: colors[i % colors.length], + bounds: size, + )); + } + + final didResize = _size != null && _size != size; + final now = DateTime.now(); + final dt = now.difference(_lastTime); + for (final snipping in _snippings) { + if (didResize) { + snipping.updateBounds(size); + } + snipping.update(dt.inMilliseconds / 1000); + snipping.draw(canvas); + } + + _size = size; + _lastTime = now; + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} + +class _ConfettiState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: ConfettiPainter( + colors: widget.colors, + animation: _controller, + ), + willChange: true, + child: const SizedBox.expand(), + ); + } + + @override + void didUpdateWidget(covariant Confetti oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.isStopped && !widget.isStopped) { + _controller.repeat(); + } else if (!oldWidget.isStopped && widget.isStopped) { + _controller.stop(canceled: false); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + _controller = AnimationController( + // We don't really care about the duration, since we're going to + // use the controller on loop anyway. + duration: const Duration(seconds: 1), + vsync: this, + ); + + if (!widget.isStopped) { + _controller.repeat(); + } + } +} + +class _PaperSnipping { + static final Random _random = Random(); + + static const degToRad = pi / 180; + + static const backSideBlend = Color(0x70EEEEEE); + + Size _bounds; + + late final _Vector position = _Vector( + _random.nextDouble() * _bounds.width, + _random.nextDouble() * _bounds.height, + ); + + final double rotationSpeed = 800 + _random.nextDouble() * 600; + + final double angle = _random.nextDouble() * 360 * degToRad; + + double rotation = _random.nextDouble() * 360 * degToRad; + + double cosA = 1.0; + + final double size = 7.0; + + final double oscillationSpeed = 0.5 + _random.nextDouble() * 1.5; + + final double xSpeed = 40; + + final double ySpeed = 50 + _random.nextDouble() * 60; + + late List<_Vector> corners = List.generate(4, (i) { + final angle = this.angle + degToRad * (45 + i * 90); + return _Vector(cos(angle), sin(angle)); + }); + + double time = _random.nextDouble(); + + final Color frontColor; + + late final Color backColor = Color.alphaBlend(backSideBlend, frontColor); + + final paint = Paint()..style = PaintingStyle.fill; + + _PaperSnipping({ + required this.frontColor, + required Size bounds, + }) : _bounds = bounds; + + void draw(Canvas canvas) { + if (cosA > 0) { + paint.color = frontColor; + } else { + paint.color = backColor; + } + + final path = Path() + ..addPolygon( + List.generate( + 4, + (index) => Offset( + position.x + corners[index].x * size, + position.y + corners[index].y * size * cosA, + )), + true, + ); + canvas.drawPath(path, paint); + } + + void update(double dt) { + time += dt; + rotation += rotationSpeed * dt; + cosA = cos(degToRad * rotation); + position.x += cos(time * oscillationSpeed) * xSpeed * dt; + position.y += ySpeed * dt; + if (position.y > _bounds.height) { + // Move the snipping back to the top. + position.x = _random.nextDouble() * _bounds.width; + position.y = 0; + } + } + + void updateBounds(Size newBounds) { + if (!newBounds.contains(Offset(position.x, position.y))) { + position.x = _random.nextDouble() * newBounds.width; + position.y = _random.nextDouble() * newBounds.height; + } + _bounds = newBounds; + } +} + +class _Vector { + double x, y; + _Vector(this.x, this.y); +} diff --git a/templates/basic/lib/style/my_button.dart b/templates/basic/lib/style/my_button.dart new file mode 100644 index 0000000..d8dce67 --- /dev/null +++ b/templates/basic/lib/style/my_button.dart @@ -0,0 +1,62 @@ +// Copyright 2023, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class MyButton extends StatefulWidget { + final Widget child; + + final VoidCallback? onPressed; + + const MyButton({super.key, required this.child, this.onPressed}); + + @override + State createState() => _MyButtonState(); +} + +class _MyButtonState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (event) { + _controller.repeat(); + }, + onExit: (event) { + _controller.stop(canceled: false); + }, + child: RotationTransition( + turns: _controller.drive(const _MySineTween(0.005)), + child: FilledButton( + onPressed: widget.onPressed, + child: widget.child, + ), + ), + ); + } +} + +class _MySineTween extends Animatable { + final double maxExtent; + + const _MySineTween(this.maxExtent); + + @override + double transform(double t) { + return sin(t * 2 * pi) * maxExtent; + } +} diff --git a/templates/basic/lib/style/my_transition.dart b/templates/basic/lib/style/my_transition.dart new file mode 100644 index 0000000..9c44d3d --- /dev/null +++ b/templates/basic/lib/style/my_transition.dart @@ -0,0 +1,77 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +CustomTransitionPage buildMyTransition({ + required Widget child, + required Color color, + String? name, + Object? arguments, + String? restorationId, + LocalKey? key, +}) { + return CustomTransitionPage( + child: child, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return _MyReveal( + animation: animation, + color: color, + child: child, + ); + }, + key: key, + name: name, + arguments: arguments, + restorationId: restorationId, + transitionDuration: const Duration(milliseconds: 700), + ); +} + +class _MyReveal extends StatelessWidget { + final Widget child; + + final Animation animation; + + final Color color; + + final _slideTween = Tween(begin: const Offset(0, -1), end: Offset.zero); + + final _fadeTween = TweenSequence([ + TweenSequenceItem(tween: ConstantTween(0.0), weight: 1), + TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 1), + ]); + + _MyReveal({ + required this.child, + required this.animation, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return Stack( + fit: StackFit.expand, + children: [ + SlideTransition( + position: _slideTween.animate( + CurvedAnimation( + parent: animation, + curve: Curves.easeOutCubic, + reverseCurve: Curves.easeOutCubic, + ), + ), + child: Container( + color: color, + ), + ), + FadeTransition( + opacity: _fadeTween.animate(animation), + child: child, + ), + ], + ); + } +} diff --git a/templates/basic/lib/style/palette.dart b/templates/basic/lib/style/palette.dart new file mode 100644 index 0000000..84de8c4 --- /dev/null +++ b/templates/basic/lib/style/palette.dart @@ -0,0 +1,37 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// A palette of colors to be used in the game. +/// +/// The reason we're not going with something like Material Design's +/// `Theme` is simply that this is simpler to work with and yet gives +/// us everything we need for a game. +/// +/// Games generally have more radical color palettes than apps. For example, +/// every level of a game can have radically different colors. +/// At the same time, games rarely support dark mode. +/// +/// Colors taken from this fun palette: +/// https://lospec.com/palette-list/crayola84 +/// +/// Colors here are implemented as getters so that hot reloading works. +/// In practice, we could just as easily implement the colors +/// as `static const`. But this way the palette is more malleable: +/// we could allow players to customize colors, for example, +/// or even get the colors from the network. +class Palette { + Color get pen => const Color(0xff1d75fb); + Color get darkPen => const Color(0xFF0050bc); + Color get redPen => const Color(0xFFd10841); + Color get inkFullOpacity => const Color(0xff352b42); + Color get ink => const Color(0xee352b42); + Color get backgroundMain => const Color(0xffffffd1); + Color get backgroundLevelSelection => const Color(0xffa2dcc7); + Color get backgroundPlaySession => const Color(0xffffebb5); + Color get background4 => const Color(0xffffd7ff); + Color get backgroundSettings => const Color(0xffbfc8e3); + Color get trueWhite => const Color(0xffffffff); +} diff --git a/templates/basic/lib/style/responsive_screen.dart b/templates/basic/lib/style/responsive_screen.dart new file mode 100644 index 0000000..a6d7ebe --- /dev/null +++ b/templates/basic/lib/style/responsive_screen.dart @@ -0,0 +1,121 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// A widget that makes it easy to create a screen with a square-ish +/// main area, a smaller menu area, and a small area for a message on top. +/// It works in both orientations on mobile- and tablet-sized screens. +class ResponsiveScreen extends StatelessWidget { + /// This is the "hero" of the screen. It's more or less square, and will + /// be placed in the visual "center" of the screen. + final Widget squarishMainArea; + + /// The second-largest area after [squarishMainArea]. It can be narrow + /// or wide. + final Widget rectangularMenuArea; + + /// An area reserved for some static text close to the top of the screen. + final Widget topMessageArea; + + const ResponsiveScreen({ + required this.squarishMainArea, + required this.rectangularMenuArea, + this.topMessageArea = const SizedBox.shrink(), + super.key, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + // This widget wants to fill the whole screen. + final size = constraints.biggest; + final padding = EdgeInsets.all(size.shortestSide / 30); + + if (size.height >= size.width) { + // "Portrait" / "mobile" mode. + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SafeArea( + bottom: false, + child: Padding( + padding: padding, + child: topMessageArea, + ), + ), + Expanded( + child: SafeArea( + top: false, + bottom: false, + minimum: padding, + child: squarishMainArea, + ), + ), + SafeArea( + top: false, + maintainBottomViewPadding: true, + child: Padding( + padding: padding, + child: Center( + child: rectangularMenuArea, + ), + ), + ), + ], + ); + } else { + // "Landscape" / "tablet" mode. + final isLarge = size.width > 900; + + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + flex: isLarge ? 7 : 5, + child: SafeArea( + right: false, + maintainBottomViewPadding: true, + minimum: padding, + child: squarishMainArea, + ), + ), + Expanded( + flex: 3, + child: Column( + children: [ + SafeArea( + bottom: false, + left: false, + maintainBottomViewPadding: true, + child: Padding( + padding: padding, + child: topMessageArea, + ), + ), + Expanded( + child: SafeArea( + top: false, + left: false, + maintainBottomViewPadding: true, + child: Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: padding, + child: rectangularMenuArea, + ), + ), + ), + ) + ], + ), + ), + ], + ); + } + }, + ); + } +} diff --git a/templates/basic/lib/win_game/win_game_screen.dart b/templates/basic/lib/win_game/win_game_screen.dart new file mode 100644 index 0000000..c34be2b --- /dev/null +++ b/templates/basic/lib/win_game/win_game_screen.dart @@ -0,0 +1,61 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../game_internals/score.dart'; +import '../style/my_button.dart'; +import '../style/palette.dart'; +import '../style/responsive_screen.dart'; + +class WinGameScreen extends StatelessWidget { + final Score score; + + const WinGameScreen({ + super.key, + required this.score, + }); + + @override + Widget build(BuildContext context) { + final palette = context.watch(); + + const gap = SizedBox(height: 10); + + return Scaffold( + backgroundColor: palette.backgroundPlaySession, + body: ResponsiveScreen( + squarishMainArea: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + gap, + const Center( + child: Text( + 'You won!', + style: TextStyle(fontFamily: 'Permanent Marker', fontSize: 50), + ), + ), + gap, + Center( + child: Text( + 'Score: ${score.score}\n' + 'Time: ${score.formattedTime}', + style: const TextStyle( + fontFamily: 'Permanent Marker', fontSize: 20), + ), + ), + ], + ), + rectangularMenuArea: MyButton( + onPressed: () { + GoRouter.of(context).go('/play'); + }, + child: const Text('Continue'), + ), + ), + ); + } +} diff --git a/templates/basic/pubspec.yaml b/templates/basic/pubspec.yaml new file mode 100644 index 0000000..819840a --- /dev/null +++ b/templates/basic/pubspec.yaml @@ -0,0 +1,48 @@ +name: basic +description: A basic game built in Flutter. + +# Prevent accidental publishing to pub.dev. +publish_to: 'none' + +version: 0.0.1+1 + +environment: + sdk: ^3.0.0 + +dependencies: + flutter: + sdk: flutter + + audioplayers: ^5.2.0 + cupertino_icons: ^1.0.6 + go_router: ^12.0.1 + logging: ^1.2.0 + provider: ^6.0.5 + shared_preferences: ^2.2.2 + +dev_dependencies: + flutter_lints: ^3.0.0 + flutter_test: + sdk: flutter + flutter_launcher_icons: ^0.13.1 + test: ^1.24.3 + +flutter: + uses-material-design: true + + assets: + - assets/images/ + - assets/music/ + - assets/sfx/ + + fonts: + - family: Permanent Marker + fonts: + - asset: assets/fonts/Permanent_Marker/PermanentMarker-Regular.ttf + +flutter_launcher_icons: + android: true + ios: true + image_path: "assets/icons/icon.png" + adaptive_icon_background: "#FFFFFF" + adaptive_icon_foreground: "assets/icons/icon-adaptive-foreground.png" diff --git a/templates/basic/test/smoke_test.dart b/templates/basic/test/smoke_test.dart new file mode 100644 index 0000000..747791c --- /dev/null +++ b/templates/basic/test/smoke_test.dart @@ -0,0 +1,40 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:basic/main.dart'; + +void main() { + testWidgets('smoke test', (tester) async { + // Build our game and trigger a frame. + await tester.pumpWidget(MyApp()); + + // Verify that the 'Play' button is shown. + expect(find.text('Play'), findsOneWidget); + + // Verify that the 'Settings' button is shown. + expect(find.text('Settings'), findsOneWidget); + + // Go to 'Settings'. + await tester.tap(find.text('Settings')); + await tester.pumpAndSettle(); + expect(find.text('Music'), findsOneWidget); + + // Go back to main menu. + await tester.tap(find.text('Back')); + await tester.pumpAndSettle(); + + // Tap 'Play'. + await tester.tap(find.text('Play')); + await tester.pumpAndSettle(); + expect(find.text('Select level'), findsOneWidget); + + // Tap level 1. + await tester.tap(find.text('Level #1')); + await tester.pumpAndSettle(); + + // Find the first level's "tutorial" text. + expect(find.text('Drag the slider to 5% or above!'), findsOneWidget); + }); +} diff --git a/templates/basic/web/favicon.png b/templates/basic/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/templates/basic/web/favicon.png differ diff --git a/templates/basic/web/icons/Icon-192.png b/templates/basic/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/templates/basic/web/icons/Icon-192.png differ diff --git a/templates/basic/web/icons/Icon-512.png b/templates/basic/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/templates/basic/web/icons/Icon-512.png differ diff --git a/templates/basic/web/icons/Icon-maskable-192.png b/templates/basic/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/templates/basic/web/icons/Icon-maskable-192.png differ diff --git a/templates/basic/web/icons/Icon-maskable-512.png b/templates/basic/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/templates/basic/web/icons/Icon-maskable-512.png differ diff --git a/templates/basic/web/index.html b/templates/basic/web/index.html new file mode 100644 index 0000000..6fcf0cd --- /dev/null +++ b/templates/basic/web/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + basic + + + + + + + + + + diff --git a/templates/basic/web/manifest.json b/templates/basic/web/manifest.json new file mode 100644 index 0000000..fdb5889 --- /dev/null +++ b/templates/basic/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "basic", + "short_name": "basic", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/templates/card/.gitignore b/templates/card/.gitignore new file mode 100644 index 0000000..24476c5 --- /dev/null +++ b/templates/card/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/templates/card/.metadata b/templates/card/.metadata new file mode 100644 index 0000000..0eb8134 --- /dev/null +++ b/templates/card/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d211f42860350d914a5ad8102f9ec32764dc6d06" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + - platform: ios + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/templates/card/README.md b/templates/card/README.md new file mode 100644 index 0000000..b5f64c9 --- /dev/null +++ b/templates/card/README.md @@ -0,0 +1,246 @@ +A starter Flutter project with a minimal shell of a board game +including the following features: + +- main menu screen +- basic navigation +- game-y theming +- settings +- sound +- drag and drop +- minimal game state management + +You can jump directly into building your game in `lib/play_session/`. + +When you're ready for things like ads, in-app purchases, achievements, +analytics, crash reporting, and so on, +there are resources ready for you +at [flutter.dev/games](https://flutter.dev/games). + + +# Getting started + +Clone this project and run the following command in its root directory: + +```terminal +flutter create . --project-name card +``` + +This will create the necessary platform files, such as `ios/`, `android/`, +`web/`, `macos/`, `linux/` or `windows/`, depending on your installation of Flutter. + +After this, the game compiles and works out of the box. It comes with things +like a main menu, a router, a settings screen, and audio. +When building a new game, this is likely everything you first need. + +When you're ready to enable more advanced integrations, +there are recipes and codelabs for you ready at +[flutter.dev/games](https://flutter.dev/games). + + +# Development + +To run the app in debug mode: + + flutter run + +It is often convenient to develop your game as a desktop app. +For example, you can run `flutter run -d macOS`, and get the same UI +in a desktop window on a Mac. That way, you don't need to use a +simulator/emulator or attach a mobile device. + + +## Code organization + +Code is organized in a loose and shallow feature-first fashion. +In `lib/`, you'll therefore find directories such as `audio`, +`main_menu` or `settings`. Nothing fancy, but usable. + +```text +lib +├── app_lifecycle +├── audio +├── game_internals +├── level_selection +├── main_menu +├── play_session +├── player_progress +├── settings +├── style +├── win_game +│ +├── main.dart +└── router.dart +``` + +The state management approach is intentionally low-level. That way, it's easy to +take this project and run with it, without having to learn new paradigms, or having +to remember to run `flutter pub run build_runner watch`. You are, +of course, encouraged to use whatever paradigm, helper package or code generation +scheme that you prefer. + + +## Building for production + +To build the app for iOS (and open Xcode when finished): + +```shell +flutter build ipa && open build/ios/archive/Runner.xcarchive +``` + +To build the app for Android (and open the folder with the bundle when finished): + +```shell +flutter build appbundle && open build/app/outputs/bundle/release +``` + +While the template is primarily meant for mobile games, you can also publish +for the web. This might be useful for web-based demos, for example, +or for rapid play-testing. The following command requires installing +[`peanut`](https://pub.dev/packages/peanut/install). + +```bash +flutter pub global run peanut \ +--web-renderer canvaskit \ +--extra-args "--base-href=/name_of_your_github_repo/" \ +&& git push origin --set-upstream gh-pages +``` + +The last line of the command above automatically pushes +your newly built web game to GitHub pages, assuming that you have +that set up. + +Lastly, it is of course possible to build your game for desktop platforms: +Windows, Linux and macOS. +Follow the [standard instructions](https://docs.flutter.dev/platform-integration/desktop). + + +# Integrations + +Focus on making your core gameplay fun first. Don't worry about +integrations like ads, in-app purchases, analytics, and so on. +It's easy to add them later, and you can find recipes and codelabs +for them at [flutter.dev/games](https://flutter.dev/games). + +Change the package name of your game +before you start any of the deeper integrations. +[StackOverflow has instructions](https://stackoverflow.com/a/51550358/1416886) +for this, and the [`rename`](https://pub.dev/packages/rename) tool +(on pub.dev) automates the process. + + +## Audio + +Audio is enabled by default and ready to go. You can modify code +in `lib/audio/` to your liking. + +You can find some music +tracks in `assets/music` — these are Creative Commons Attribution (CC-BY) +licensed, and are included in this repository with permission. If you decide +to keep these tracks in your game, please don't forget to give credit +to the musician, [Mr Smith][]. + +[Mr Smith]: https://freemusicarchive.org/music/mr-smith + +The repository also includes a few sound effect samples in `assets/sfx`. +These are public domain (CC0) and you will almost surely want to replace +them because they're just recordings of a developer doing silly sounds +with their mouth. + +## Logging + +The template uses the [`logging`](https://pub.dev/packages/logging) package +to log messages to the console. This makes it very easy to log messages +from anywhere with something like the following: + +```dart +import 'package:logging/logging.dart'; + +final _log = Logger('Foo'); + +void foo() { + _log.info('Hello, world!'); +} +``` + +This will show up in the console as: + +```text +[Foo] Hello, world! +``` + +When using Flutter DevTools, all the metadata of the log message is preserved, +so you can filter by logger name, log level, and so on. + +Later, when you're closer to production, you can gather these log messages +(see `lib/main.dart`) and send them to a service like Firebase Crashlytics +when appropriate. +See [`firebase_crashlytics`](https://pub.dev/packages/firebase_crashlytics) +for more information. + + +## Settings + +The settings page is enabled by default, and accessible both +from the main menu and through the "gear" button in the play session screen. + +Settings are saved to local storage using the +[`shared_preferences`](https://pub.dev/packages/shared_preferences) +package. +To change what preferences are saved and how, edit files in +`lib/src/settings/persistence`. + + +# Icon + +To update the launcher icon, first change the files +`assets/icon-adaptive-foreground.png` and `assets/icon.png`. +Then, run the following: + +```bash +flutter pub run flutter_launcher_icons:main +``` + +You can [configure](https://github.com/fluttercommunity/flutter_launcher_icons#book-guide) +the look of the icon in the `flutter_icons:` section of `pubspec.yaml`. + + +# Troubleshooting + +## CocoaPods + +When upgrading to higher versions of Flutter or plugins, you might encounter an error when +building the iOS or macOS app. A good first thing to try is to delete the `ios/Podfile.lock` +file (or `macos/Podfile.lock`, respectively), then trying to build again. (You can achieve +a more thorough cleanup by running `flutter clean` instead.) + +If this doesn't help, here are some more methods: + +- See if everything is still okay with your Flutter and CocoaPods installation + by running `flutter doctor`. Revisit the macOS + [Flutter installation guide](https://docs.flutter.dev/get-started/install/macos) + if needed. +- Update CocoaPods specs directory: + + ```sh + cd ios + pod repo update + cd .. + ``` + + (Substitute `ios` for `macos` when appropriate.) +- Open the project in Xcode, + [increase the build target](https://stackoverflow.com/a/38602597/1416886), + then select _Product_ > _Clean Build Folder_. + +## Warnings in console + +When running the game for the first time, you might see warnings like the following: + +> Note: Some input files use or override a deprecated API. + +or + +> warning: 'viewState' was deprecated in macOS 11.0: Use -initWithState: instead + +These warning come from the various plugins that are used by the template. They are not harmful +and can be ignored. The warnings are meant for the plugin authors, not for you, the game developer. diff --git a/templates/card/analysis_options.yaml b/templates/card/analysis_options.yaml new file mode 100644 index 0000000..12b7a31 --- /dev/null +++ b/templates/card/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + # Remove or force lint rules by adding lines like the following. + # The lints below are disabled in order to make things smoother in early + # development. Consider enabling them once development is further along. + prefer_const_constructors: false # Annoying in early development + prefer_single_quotes: false # Annoying in early development diff --git a/templates/card/android/.gitignore b/templates/card/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/templates/card/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/templates/card/android/app/build.gradle b/templates/card/android/app/build.gradle new file mode 100644 index 0000000..4cd8849 --- /dev/null +++ b/templates/card/android/app/build.gradle @@ -0,0 +1,67 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + namespace "com.example.card" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.card" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies {} diff --git a/templates/card/android/app/src/debug/AndroidManifest.xml b/templates/card/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/templates/card/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/templates/card/android/app/src/main/AndroidManifest.xml b/templates/card/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b9f0128 --- /dev/null +++ b/templates/card/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/templates/card/android/app/src/main/kotlin/com/example/card/MainActivity.kt b/templates/card/android/app/src/main/kotlin/com/example/card/MainActivity.kt new file mode 100644 index 0000000..34859b9 --- /dev/null +++ b/templates/card/android/app/src/main/kotlin/com/example/card/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.card + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/templates/card/android/app/src/main/res/drawable-v21/launch_background.xml b/templates/card/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/templates/card/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/templates/card/android/app/src/main/res/drawable/launch_background.xml b/templates/card/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/templates/card/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/templates/card/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/templates/card/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/templates/card/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/templates/card/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/templates/card/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/templates/card/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/templates/card/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/templates/card/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/templates/card/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/templates/card/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/templates/card/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/templates/card/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/templates/card/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/templates/card/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/templates/card/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/templates/card/android/app/src/main/res/values-night/styles.xml b/templates/card/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/templates/card/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/templates/card/android/app/src/main/res/values/styles.xml b/templates/card/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/templates/card/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/templates/card/android/app/src/profile/AndroidManifest.xml b/templates/card/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/templates/card/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/templates/card/android/build.gradle b/templates/card/android/build.gradle new file mode 100644 index 0000000..f7eb7f6 --- /dev/null +++ b/templates/card/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/templates/card/android/gradle.properties b/templates/card/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/templates/card/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/templates/card/android/gradle/wrapper/gradle-wrapper.properties b/templates/card/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3c472b9 --- /dev/null +++ b/templates/card/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/templates/card/android/settings.gradle b/templates/card/android/settings.gradle new file mode 100644 index 0000000..55c4ca8 --- /dev/null +++ b/templates/card/android/settings.gradle @@ -0,0 +1,20 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() + + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + + plugins { + id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false + } +} + +include ":app" + +apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/templates/card/assets/fonts/Permanent_Marker/LICENSE.txt b/templates/card/assets/fonts/Permanent_Marker/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/templates/card/assets/fonts/Permanent_Marker/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + 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. diff --git a/templates/card/assets/fonts/Permanent_Marker/PermanentMarker-Regular.ttf b/templates/card/assets/fonts/Permanent_Marker/PermanentMarker-Regular.ttf new file mode 100644 index 0000000..6541e9d Binary files /dev/null and b/templates/card/assets/fonts/Permanent_Marker/PermanentMarker-Regular.ttf differ diff --git a/templates/card/assets/icons/icon-adaptive-foreground.png b/templates/card/assets/icons/icon-adaptive-foreground.png new file mode 100644 index 0000000..6fe9455 Binary files /dev/null and b/templates/card/assets/icons/icon-adaptive-foreground.png differ diff --git a/templates/card/assets/icons/icon.png b/templates/card/assets/icons/icon.png new file mode 100644 index 0000000..a8f5c30 Binary files /dev/null and b/templates/card/assets/icons/icon.png differ diff --git a/templates/card/assets/images/2x/back.png b/templates/card/assets/images/2x/back.png new file mode 100644 index 0000000..c2daa28 Binary files /dev/null and b/templates/card/assets/images/2x/back.png differ diff --git a/templates/card/assets/images/2x/restart.png b/templates/card/assets/images/2x/restart.png new file mode 100644 index 0000000..360c109 Binary files /dev/null and b/templates/card/assets/images/2x/restart.png differ diff --git a/templates/card/assets/images/2x/settings.png b/templates/card/assets/images/2x/settings.png new file mode 100644 index 0000000..8780f94 Binary files /dev/null and b/templates/card/assets/images/2x/settings.png differ diff --git a/templates/card/assets/images/3.5x/back.png b/templates/card/assets/images/3.5x/back.png new file mode 100644 index 0000000..5961b22 Binary files /dev/null and b/templates/card/assets/images/3.5x/back.png differ diff --git a/templates/card/assets/images/3.5x/restart.png b/templates/card/assets/images/3.5x/restart.png new file mode 100644 index 0000000..3f89e19 Binary files /dev/null and b/templates/card/assets/images/3.5x/restart.png differ diff --git a/templates/card/assets/images/3.5x/settings.png b/templates/card/assets/images/3.5x/settings.png new file mode 100644 index 0000000..33d4326 Binary files /dev/null and b/templates/card/assets/images/3.5x/settings.png differ diff --git a/templates/card/assets/images/3x/back.png b/templates/card/assets/images/3x/back.png new file mode 100644 index 0000000..2ac088b Binary files /dev/null and b/templates/card/assets/images/3x/back.png differ diff --git a/templates/card/assets/images/3x/restart.png b/templates/card/assets/images/3x/restart.png new file mode 100644 index 0000000..3578b92 Binary files /dev/null and b/templates/card/assets/images/3x/restart.png differ diff --git a/templates/card/assets/images/3x/settings.png b/templates/card/assets/images/3x/settings.png new file mode 100644 index 0000000..a7dbd26 Binary files /dev/null and b/templates/card/assets/images/3x/settings.png differ diff --git a/templates/card/assets/images/back.png b/templates/card/assets/images/back.png new file mode 100644 index 0000000..65ab3ee Binary files /dev/null and b/templates/card/assets/images/back.png differ diff --git a/templates/card/assets/images/restart.png b/templates/card/assets/images/restart.png new file mode 100644 index 0000000..bd22efe Binary files /dev/null and b/templates/card/assets/images/restart.png differ diff --git a/templates/card/assets/images/settings.png b/templates/card/assets/images/settings.png new file mode 100644 index 0000000..48863f4 Binary files /dev/null and b/templates/card/assets/images/settings.png differ diff --git a/templates/card/assets/music/Mr_Smith-Azul.mp3 b/templates/card/assets/music/Mr_Smith-Azul.mp3 new file mode 100644 index 0000000..d454d1a Binary files /dev/null and b/templates/card/assets/music/Mr_Smith-Azul.mp3 differ diff --git a/templates/card/assets/music/Mr_Smith-Sonorus.mp3 b/templates/card/assets/music/Mr_Smith-Sonorus.mp3 new file mode 100644 index 0000000..28a01d7 Binary files /dev/null and b/templates/card/assets/music/Mr_Smith-Sonorus.mp3 differ diff --git a/templates/card/assets/music/Mr_Smith-Sunday_Solitude.mp3 b/templates/card/assets/music/Mr_Smith-Sunday_Solitude.mp3 new file mode 100644 index 0000000..0eec639 Binary files /dev/null and b/templates/card/assets/music/Mr_Smith-Sunday_Solitude.mp3 differ diff --git a/templates/card/assets/music/README.md b/templates/card/assets/music/README.md new file mode 100644 index 0000000..c054c07 --- /dev/null +++ b/templates/card/assets/music/README.md @@ -0,0 +1,6 @@ +Music in the template is by Mr Smith, and licensed under Creative Commons +Attribution 4.0 International (CC BY 4.0). + +https://freemusicarchive.org/music/mr-smith + +Mr Smith's music is used in this template project with his explicit permission. diff --git a/templates/card/assets/sfx/README.md b/templates/card/assets/sfx/README.md new file mode 100644 index 0000000..9009909 --- /dev/null +++ b/templates/card/assets/sfx/README.md @@ -0,0 +1 @@ +Sounds in this folder are made by Filip Hracek and are CC0 (Public Domain). diff --git a/templates/card/assets/sfx/dsht1.mp3 b/templates/card/assets/sfx/dsht1.mp3 new file mode 100644 index 0000000..8ce3ac5 Binary files /dev/null and b/templates/card/assets/sfx/dsht1.mp3 differ diff --git a/templates/card/assets/sfx/ehehee1.mp3 b/templates/card/assets/sfx/ehehee1.mp3 new file mode 100644 index 0000000..73d6ccf Binary files /dev/null and b/templates/card/assets/sfx/ehehee1.mp3 differ diff --git a/templates/card/assets/sfx/fwfwfwfw1.mp3 b/templates/card/assets/sfx/fwfwfwfw1.mp3 new file mode 100644 index 0000000..2784578 Binary files /dev/null and b/templates/card/assets/sfx/fwfwfwfw1.mp3 differ diff --git a/templates/card/assets/sfx/fwfwfwfwfw1.mp3 b/templates/card/assets/sfx/fwfwfwfwfw1.mp3 new file mode 100644 index 0000000..f847701 Binary files /dev/null and b/templates/card/assets/sfx/fwfwfwfwfw1.mp3 differ diff --git a/templates/card/assets/sfx/hash1.mp3 b/templates/card/assets/sfx/hash1.mp3 new file mode 100644 index 0000000..5e4bb7c Binary files /dev/null and b/templates/card/assets/sfx/hash1.mp3 differ diff --git a/templates/card/assets/sfx/hash2.mp3 b/templates/card/assets/sfx/hash2.mp3 new file mode 100644 index 0000000..1068ce4 Binary files /dev/null and b/templates/card/assets/sfx/hash2.mp3 differ diff --git a/templates/card/assets/sfx/hash3.mp3 b/templates/card/assets/sfx/hash3.mp3 new file mode 100644 index 0000000..916ddda Binary files /dev/null and b/templates/card/assets/sfx/hash3.mp3 differ diff --git a/templates/card/assets/sfx/haw1.mp3 b/templates/card/assets/sfx/haw1.mp3 new file mode 100644 index 0000000..7d25af2 Binary files /dev/null and b/templates/card/assets/sfx/haw1.mp3 differ diff --git a/templates/card/assets/sfx/hh1.mp3 b/templates/card/assets/sfx/hh1.mp3 new file mode 100644 index 0000000..8ca4344 Binary files /dev/null and b/templates/card/assets/sfx/hh1.mp3 differ diff --git a/templates/card/assets/sfx/hh2.mp3 b/templates/card/assets/sfx/hh2.mp3 new file mode 100644 index 0000000..b11cb0a Binary files /dev/null and b/templates/card/assets/sfx/hh2.mp3 differ diff --git a/templates/card/assets/sfx/k1.mp3 b/templates/card/assets/sfx/k1.mp3 new file mode 100644 index 0000000..757e21e Binary files /dev/null and b/templates/card/assets/sfx/k1.mp3 differ diff --git a/templates/card/assets/sfx/k2.mp3 b/templates/card/assets/sfx/k2.mp3 new file mode 100644 index 0000000..1e45f8b Binary files /dev/null and b/templates/card/assets/sfx/k2.mp3 differ diff --git a/templates/card/assets/sfx/kch1.mp3 b/templates/card/assets/sfx/kch1.mp3 new file mode 100644 index 0000000..cfe79c3 Binary files /dev/null and b/templates/card/assets/sfx/kch1.mp3 differ diff --git a/templates/card/assets/sfx/kss1.mp3 b/templates/card/assets/sfx/kss1.mp3 new file mode 100644 index 0000000..e7fd20a Binary files /dev/null and b/templates/card/assets/sfx/kss1.mp3 differ diff --git a/templates/card/assets/sfx/lalala1.mp3 b/templates/card/assets/sfx/lalala1.mp3 new file mode 100644 index 0000000..85dcd80 Binary files /dev/null and b/templates/card/assets/sfx/lalala1.mp3 differ diff --git a/templates/card/assets/sfx/oo1.mp3 b/templates/card/assets/sfx/oo1.mp3 new file mode 100644 index 0000000..172ca82 Binary files /dev/null and b/templates/card/assets/sfx/oo1.mp3 differ diff --git a/templates/card/assets/sfx/p1.mp3 b/templates/card/assets/sfx/p1.mp3 new file mode 100644 index 0000000..6575546 Binary files /dev/null and b/templates/card/assets/sfx/p1.mp3 differ diff --git a/templates/card/assets/sfx/p2.mp3 b/templates/card/assets/sfx/p2.mp3 new file mode 100644 index 0000000..7d7a7fd Binary files /dev/null and b/templates/card/assets/sfx/p2.mp3 differ diff --git a/templates/card/assets/sfx/sh1.mp3 b/templates/card/assets/sfx/sh1.mp3 new file mode 100644 index 0000000..5059dd5 Binary files /dev/null and b/templates/card/assets/sfx/sh1.mp3 differ diff --git a/templates/card/assets/sfx/sh2.mp3 b/templates/card/assets/sfx/sh2.mp3 new file mode 100644 index 0000000..6425d99 Binary files /dev/null and b/templates/card/assets/sfx/sh2.mp3 differ diff --git a/templates/card/assets/sfx/spsh1.mp3 b/templates/card/assets/sfx/spsh1.mp3 new file mode 100644 index 0000000..34065c8 Binary files /dev/null and b/templates/card/assets/sfx/spsh1.mp3 differ diff --git a/templates/card/assets/sfx/swishswish1.mp3 b/templates/card/assets/sfx/swishswish1.mp3 new file mode 100644 index 0000000..2d030aa Binary files /dev/null and b/templates/card/assets/sfx/swishswish1.mp3 differ diff --git a/templates/card/assets/sfx/wehee1.mp3 b/templates/card/assets/sfx/wehee1.mp3 new file mode 100644 index 0000000..8f7bc84 Binary files /dev/null and b/templates/card/assets/sfx/wehee1.mp3 differ diff --git a/templates/card/assets/sfx/ws1.mp3 b/templates/card/assets/sfx/ws1.mp3 new file mode 100644 index 0000000..0daf26a Binary files /dev/null and b/templates/card/assets/sfx/ws1.mp3 differ diff --git a/templates/card/assets/sfx/wssh1.mp3 b/templates/card/assets/sfx/wssh1.mp3 new file mode 100644 index 0000000..9f455d9 Binary files /dev/null and b/templates/card/assets/sfx/wssh1.mp3 differ diff --git a/templates/card/assets/sfx/wssh2.mp3 b/templates/card/assets/sfx/wssh2.mp3 new file mode 100644 index 0000000..b5c5c3b Binary files /dev/null and b/templates/card/assets/sfx/wssh2.mp3 differ diff --git a/templates/card/assets/sfx/yay1.mp3 b/templates/card/assets/sfx/yay1.mp3 new file mode 100644 index 0000000..51102cb Binary files /dev/null and b/templates/card/assets/sfx/yay1.mp3 differ diff --git a/templates/card/ios/.gitignore b/templates/card/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/templates/card/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/templates/card/ios/Flutter/AppFrameworkInfo.plist b/templates/card/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/templates/card/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/templates/card/ios/Flutter/Debug.xcconfig b/templates/card/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/templates/card/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/templates/card/ios/Flutter/Release.xcconfig b/templates/card/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/templates/card/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/templates/card/ios/Podfile b/templates/card/ios/Podfile new file mode 100644 index 0000000..fdcc671 --- /dev/null +++ b/templates/card/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/templates/card/ios/Runner.xcodeproj/project.pbxproj b/templates/card/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..3b955c3 --- /dev/null +++ b/templates/card/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,614 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807E294A63A400263BE5 /* Frameworks */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.card; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AE0B7B92F70575B8D7E0D07E /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.card.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 89B67EB44CE7B6631473024E /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.card.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 640959BDD8F10B91D80A66BE /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.card.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.card; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.card; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/templates/card/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/templates/card/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/templates/card/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/templates/card/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/templates/card/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/templates/card/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/templates/card/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/templates/card/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/templates/card/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/templates/card/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/templates/card/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..87131a0 --- /dev/null +++ b/templates/card/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/card/ios/Runner.xcworkspace/contents.xcworkspacedata b/templates/card/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/templates/card/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/templates/card/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/templates/card/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/templates/card/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/templates/card/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/templates/card/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/templates/card/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/templates/card/ios/Runner/AppDelegate.swift b/templates/card/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/templates/card/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/templates/card/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/templates/card/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/templates/card/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/templates/card/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/templates/card/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/templates/card/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/templates/card/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/templates/card/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/templates/card/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/templates/card/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/templates/card/ios/Runner/Base.lproj/LaunchScreen.storyboard b/templates/card/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/templates/card/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/card/ios/Runner/Base.lproj/Main.storyboard b/templates/card/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/templates/card/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/card/ios/Runner/Info.plist b/templates/card/ios/Runner/Info.plist new file mode 100644 index 0000000..5a35089 --- /dev/null +++ b/templates/card/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Card + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + card + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/templates/card/ios/Runner/Runner-Bridging-Header.h b/templates/card/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/templates/card/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/templates/card/ios/RunnerTests/RunnerTests.swift b/templates/card/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/templates/card/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/templates/card/lib/app_lifecycle/app_lifecycle.dart b/templates/card/lib/app_lifecycle/app_lifecycle.dart new file mode 100644 index 0000000..641d40f --- /dev/null +++ b/templates/card/lib/app_lifecycle/app_lifecycle.dart @@ -0,0 +1,67 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:logging/logging.dart'; +import 'package:provider/provider.dart'; + +typedef AppLifecycleStateNotifier = ValueNotifier; + +// TODO(filiph): use the new AppLifecycleListener class, e.g. +// https://kazlauskas.dev/flutter-app-lifecycle-listener-overview/ +class AppLifecycleObserver extends StatefulWidget { + final Widget child; + + const AppLifecycleObserver({required this.child, super.key}); + + @override + State createState() => _AppLifecycleObserverState(); +} + +class _AppLifecycleObserverState extends State + with WidgetsBindingObserver { + static final _log = Logger('AppLifecycleObserver'); + + final ValueNotifier lifecycleListenable = + ValueNotifier(AppLifecycleState.inactive); + + @override + Widget build(BuildContext context) { + // Using InheritedProvider because we don't want to use Consumer + // or context.watch or anything like that to listen to this. We want + // to manually add listeners. We're interested in the _events_ of lifecycle + // state changes, and not so much in the state itself. (For example, + // we want to stop sound when the app goes into the background, and + // restart sound again when the app goes back into focus. We're not + // rebuilding any widgets.) + // + // Provider, by default, throws when one + // is trying to provide a Listenable (such as ValueNotifier) without using + // something like ValueListenableProvider. InheritedProvider is more + // low-level and doesn't have this problem. + return InheritedProvider.value( + value: lifecycleListenable, + child: widget.child, + ); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + _log.info(() => 'didChangeAppLifecycleState: $state'); + lifecycleListenable.value = state; + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _log.info('Subscribed to app lifecycle updates'); + } +} diff --git a/templates/card/lib/audio/audio_controller.dart b/templates/card/lib/audio/audio_controller.dart new file mode 100644 index 0000000..0e05c7c --- /dev/null +++ b/templates/card/lib/audio/audio_controller.dart @@ -0,0 +1,270 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:collection'; +import 'dart:math'; + +import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:logging/logging.dart'; + +import '../app_lifecycle/app_lifecycle.dart'; +import '../settings/settings.dart'; +import 'songs.dart'; +import 'sounds.dart'; + +/// Allows playing music and sound. A facade to `package:audioplayers`. +class AudioController { + static final _log = Logger('AudioController'); + + final AudioPlayer _musicPlayer; + + /// This is a list of [AudioPlayer] instances which are rotated to play + /// sound effects. + final List _sfxPlayers; + + int _currentSfxPlayer = 0; + + final Queue _playlist; + + final Random _random = Random(); + + SettingsController? _settings; + + ValueNotifier? _lifecycleNotifier; + + /// Creates an instance that plays music and sound. + /// + /// Use [polyphony] to configure the number of sound effects (SFX) that can + /// play at the same time. A [polyphony] of `1` will always only play one + /// sound (a new sound will stop the previous one). See discussion + /// of [_sfxPlayers] to learn why this is the case. + /// + /// Background music does not count into the [polyphony] limit. Music will + /// never be overridden by sound effects because that would be silly. + AudioController({int polyphony = 2}) + : assert(polyphony >= 1), + _musicPlayer = AudioPlayer(playerId: 'musicPlayer'), + _sfxPlayers = Iterable.generate( + polyphony, (i) => AudioPlayer(playerId: 'sfxPlayer#$i')) + .toList(growable: false), + _playlist = Queue.of(List.of(songs)..shuffle()) { + _musicPlayer.onPlayerComplete.listen(_handleSongFinished); + unawaited(_preloadSfx()); + } + + /// Makes sure the audio controller is listening to changes + /// of both the app lifecycle (e.g. suspended app) and to changes + /// of settings (e.g. muted sound). + void attachDependencies(AppLifecycleStateNotifier lifecycleNotifier, + SettingsController settingsController) { + _attachLifecycleNotifier(lifecycleNotifier); + _attachSettings(settingsController); + } + + void dispose() { + _lifecycleNotifier?.removeListener(_handleAppLifecycle); + _stopAllSound(); + _musicPlayer.dispose(); + for (final player in _sfxPlayers) { + player.dispose(); + } + } + + /// Plays a single sound effect, defined by [type]. + /// + /// The controller will ignore this call when the attached settings' + /// [SettingsController.audioOn] is `true` or if its + /// [SettingsController.soundsOn] is `false`. + void playSfx(SfxType type) { + final audioOn = _settings?.audioOn.value ?? false; + if (!audioOn) { + _log.fine(() => 'Ignoring playing sound ($type) because audio is muted.'); + return; + } + final soundsOn = _settings?.soundsOn.value ?? false; + if (!soundsOn) { + _log.fine(() => + 'Ignoring playing sound ($type) because sounds are turned off.'); + return; + } + + _log.fine(() => 'Playing sound: $type'); + final options = soundTypeToFilename(type); + final filename = options[_random.nextInt(options.length)]; + _log.fine(() => '- Chosen filename: $filename'); + + final currentPlayer = _sfxPlayers[_currentSfxPlayer]; + currentPlayer.play(AssetSource('sfx/$filename'), + volume: soundTypeToVolume(type)); + _currentSfxPlayer = (_currentSfxPlayer + 1) % _sfxPlayers.length; + } + + /// Enables the [AudioController] to listen to [AppLifecycleState] events, + /// and therefore do things like stopping playback when the game + /// goes into the background. + void _attachLifecycleNotifier(AppLifecycleStateNotifier lifecycleNotifier) { + _lifecycleNotifier?.removeListener(_handleAppLifecycle); + + lifecycleNotifier.addListener(_handleAppLifecycle); + _lifecycleNotifier = lifecycleNotifier; + } + + /// Enables the [AudioController] to track changes to settings. + /// Namely, when any of [SettingsController.audioOn], + /// [SettingsController.musicOn] or [SettingsController.soundsOn] changes, + /// the audio controller will act accordingly. + void _attachSettings(SettingsController settingsController) { + if (_settings == settingsController) { + // Already attached to this instance. Nothing to do. + return; + } + + // Remove handlers from the old settings controller if present + final oldSettings = _settings; + if (oldSettings != null) { + oldSettings.audioOn.removeListener(_audioOnHandler); + oldSettings.musicOn.removeListener(_musicOnHandler); + oldSettings.soundsOn.removeListener(_soundsOnHandler); + } + + _settings = settingsController; + + // Add handlers to the new settings controller + settingsController.audioOn.addListener(_audioOnHandler); + settingsController.musicOn.addListener(_musicOnHandler); + settingsController.soundsOn.addListener(_soundsOnHandler); + + if (settingsController.audioOn.value && settingsController.musicOn.value) { + if (kIsWeb) { + _log.info('On the web, music can only start after user interaction.'); + } else { + _playCurrentSongInPlaylist(); + } + } + } + + void _audioOnHandler() { + _log.fine('audioOn changed to ${_settings!.audioOn.value}'); + if (_settings!.audioOn.value) { + // All sound just got un-muted. Audio is on. + if (_settings!.musicOn.value) { + _startOrResumeMusic(); + } + } else { + // All sound just got muted. Audio is off. + _stopAllSound(); + } + } + + void _handleAppLifecycle() { + switch (_lifecycleNotifier!.value) { + case AppLifecycleState.paused: + case AppLifecycleState.detached: + case AppLifecycleState.hidden: + _stopAllSound(); + case AppLifecycleState.resumed: + if (_settings!.audioOn.value && _settings!.musicOn.value) { + _startOrResumeMusic(); + } + case AppLifecycleState.inactive: + // No need to react to this state change. + break; + } + } + + void _handleSongFinished(void _) { + _log.info('Last song finished playing.'); + // Move the song that just finished playing to the end of the playlist. + _playlist.addLast(_playlist.removeFirst()); + // Play the song at the beginning of the playlist. + _playCurrentSongInPlaylist(); + } + + void _musicOnHandler() { + if (_settings!.musicOn.value) { + // Music got turned on. + if (_settings!.audioOn.value) { + _startOrResumeMusic(); + } + } else { + // Music got turned off. + _musicPlayer.pause(); + } + } + + Future _playCurrentSongInPlaylist() async { + _log.info(() => 'Playing ${_playlist.first} now.'); + try { + await _musicPlayer.play(AssetSource('music/${_playlist.first.filename}')); + } catch (e) { + _log.severe('Could not play song ${_playlist.first}', e); + } + + // Settings can change while the music player is preparing + // to play a song (i.e. during the `await` above). + // Unfortunately, `audioplayers` has a bug which will ignore calls + // to `pause()` before that await is finished, so we need + // to double check here. + // See issue: https://github.com/bluefireteam/audioplayers/issues/1687 + if (!_settings!.audioOn.value || !_settings!.musicOn.value) { + try { + _log.fine('Settings changed while preparing to play song. ' + 'Pausing music.'); + await _musicPlayer.pause(); + } catch (e) { + _log.severe('Could not pause music player', e); + } + } + } + + /// Preloads all sound effects. + Future _preloadSfx() async { + _log.info('Preloading sound effects'); + // This assumes there is only a limited number of sound effects in the game. + // If there are hundreds of long sound effect files, it's better + // to be more selective when preloading. + await AudioCache.instance.loadAll(SfxType.values + .expand(soundTypeToFilename) + .map((path) => 'sfx/$path') + .toList()); + } + + void _soundsOnHandler() { + for (final player in _sfxPlayers) { + if (player.state == PlayerState.playing) { + player.stop(); + } + } + } + + void _startOrResumeMusic() async { + if (_musicPlayer.source == null) { + _log.info('No music source set. ' + 'Start playing the current song in playlist.'); + await _playCurrentSongInPlaylist(); + return; + } + + _log.info('Resuming paused music.'); + try { + _musicPlayer.resume(); + } catch (e) { + // Sometimes, resuming fails with an "Unexpected" error. + _log.severe("Error resuming music", e); + // Try starting the song from scratch. + _playCurrentSongInPlaylist(); + } + } + + void _stopAllSound() { + _log.info('Stopping all sound'); + _musicPlayer.pause(); + for (final player in _sfxPlayers) { + player.stop(); + } + } +} diff --git a/templates/card/lib/audio/songs.dart b/templates/card/lib/audio/songs.dart new file mode 100644 index 0000000..dd542bd --- /dev/null +++ b/templates/card/lib/audio/songs.dart @@ -0,0 +1,24 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +const Set songs = { + // Filenames with whitespace break package:audioplayers on iOS + // (as of February 2022), so we use no whitespace. + Song('Mr_Smith-Azul.mp3', 'Azul', artist: 'Mr Smith'), + Song('Mr_Smith-Sonorus.mp3', 'Sonorus', artist: 'Mr Smith'), + Song('Mr_Smith-Sunday_Solitude.mp3', 'SundaySolitude', artist: 'Mr Smith'), +}; + +class Song { + final String filename; + + final String name; + + final String? artist; + + const Song(this.filename, this.name, {this.artist}); + + @override + String toString() => 'Song<$filename>'; +} diff --git a/templates/card/lib/audio/sounds.dart b/templates/card/lib/audio/sounds.dart new file mode 100644 index 0000000..139ac21 --- /dev/null +++ b/templates/card/lib/audio/sounds.dart @@ -0,0 +1,71 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +List soundTypeToFilename(SfxType type) { + switch (type) { + case SfxType.huhsh: + return const [ + 'hash1.mp3', + 'hash2.mp3', + 'hash3.mp3', + ]; + case SfxType.wssh: + return const [ + 'wssh1.mp3', + 'wssh2.mp3', + 'dsht1.mp3', + 'ws1.mp3', + 'spsh1.mp3', + 'hh1.mp3', + 'hh2.mp3', + 'kss1.mp3', + ]; + case SfxType.buttonTap: + return const [ + 'k1.mp3', + 'k2.mp3', + 'p1.mp3', + 'p2.mp3', + ]; + case SfxType.congrats: + return const [ + 'yay1.mp3', + 'wehee1.mp3', + 'oo1.mp3', + ]; + case SfxType.erase: + return const [ + 'fwfwfwfwfw1.mp3', + 'fwfwfwfw1.mp3', + ]; + case SfxType.swishSwish: + return const [ + 'swishswish1.mp3', + ]; + } +} + +/// Allows control over loudness of different SFX types. +double soundTypeToVolume(SfxType type) { + switch (type) { + case SfxType.huhsh: + return 0.4; + case SfxType.wssh: + return 0.2; + case SfxType.buttonTap: + case SfxType.congrats: + case SfxType.erase: + case SfxType.swishSwish: + return 1.0; + } +} + +enum SfxType { + huhsh, + wssh, + buttonTap, + congrats, + erase, + swishSwish, +} diff --git a/templates/card/lib/game_internals/board_state.dart b/templates/card/lib/game_internals/board_state.dart new file mode 100644 index 0000000..841fc47 --- /dev/null +++ b/templates/card/lib/game_internals/board_state.dart @@ -0,0 +1,36 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/foundation.dart'; + +import 'player.dart'; +import 'playing_area.dart'; + +class BoardState { + final VoidCallback onWin; + + final PlayingArea areaOne = PlayingArea(); + + final PlayingArea areaTwo = PlayingArea(); + + final Player player = Player(); + + BoardState({required this.onWin}) { + player.addListener(_handlePlayerChange); + } + + List get areas => [areaOne, areaTwo]; + + void dispose() { + player.removeListener(_handlePlayerChange); + areaOne.dispose(); + areaTwo.dispose(); + } + + void _handlePlayerChange() { + if (player.hand.isEmpty) { + onWin(); + } + } +} diff --git a/templates/card/lib/game_internals/card_suit.dart b/templates/card/lib/game_internals/card_suit.dart new file mode 100644 index 0000000..1254d38 --- /dev/null +++ b/templates/card/lib/game_internals/card_suit.dart @@ -0,0 +1,42 @@ +enum CardSuit { + clubs(1), + spades(2), + hearts(3), + diamonds(4); + + final int internalRepresentation; + + const CardSuit(this.internalRepresentation); + + String get asCharacter { + switch (this) { + case CardSuit.spades: + return '♠'; + case CardSuit.hearts: + return '♥'; + case CardSuit.diamonds: + return '♦'; + case CardSuit.clubs: + return '♣'; + } + } + + CardSuitColor get color { + switch (this) { + case CardSuit.spades: + case CardSuit.clubs: + return CardSuitColor.black; + case CardSuit.hearts: + case CardSuit.diamonds: + return CardSuitColor.red; + } + } + + @override + String toString() => asCharacter; +} + +enum CardSuitColor { + black, + red, +} diff --git a/templates/card/lib/game_internals/player.dart b/templates/card/lib/game_internals/player.dart new file mode 100644 index 0000000..982d7a6 --- /dev/null +++ b/templates/card/lib/game_internals/player.dart @@ -0,0 +1,15 @@ +import 'package:flutter/foundation.dart'; + +import 'playing_card.dart'; + +class Player extends ChangeNotifier { + static const maxCards = 7; + + final List hand = + List.generate(maxCards, (index) => PlayingCard.random()); + + void removeCard(PlayingCard card) { + hand.remove(card); + notifyListeners(); + } +} diff --git a/templates/card/lib/game_internals/playing_area.dart b/templates/card/lib/game_internals/playing_area.dart new file mode 100644 index 0000000..223c80a --- /dev/null +++ b/templates/card/lib/game_internals/playing_area.dart @@ -0,0 +1,69 @@ +import 'dart:async'; + +import 'package:async/async.dart'; + +import 'playing_card.dart'; + +class PlayingArea { + /// The maximum number of cards in this playing area. + static const int maxCards = 6; + + /// The current cards in this area. + final List cards = []; + + final StreamController _playerChanges = + StreamController.broadcast(); + + final StreamController _remoteChanges = + StreamController.broadcast(); + + PlayingArea(); + + /// A [Stream] that fires an event every time any change to this area is made. + Stream get allChanges => + StreamGroup.mergeBroadcast([remoteChanges, playerChanges]); + + /// A [Stream] that fires an event every time a change is made _locally_, + /// by the player. + Stream get playerChanges => _playerChanges.stream; + + /// A [Stream] that fires an event every time a change is made _remotely_, + /// by another player. + Stream get remoteChanges => _remoteChanges.stream; + + /// Accepts the [card] into the area. + void acceptCard(PlayingCard card) { + cards.add(card); + _maybeTrim(); + _playerChanges.add(null); + } + + void dispose() { + _remoteChanges.close(); + _playerChanges.close(); + } + + /// Removes the first card in the area, if any. + void removeFirstCard() { + if (cards.isEmpty) return; + cards.removeAt(0); + _playerChanges.add(null); + } + + /// Replaces the cards in the area with [cards]. + /// + /// This method is meant to be called when the cards are updated from + /// a server. + void replaceWith(List cards) { + this.cards.clear(); + this.cards.addAll(cards); + _maybeTrim(); + _remoteChanges.add(null); + } + + void _maybeTrim() { + if (cards.length > maxCards) { + cards.removeRange(0, cards.length - maxCards); + } + } +} diff --git a/templates/card/lib/game_internals/playing_card.dart b/templates/card/lib/game_internals/playing_card.dart new file mode 100644 index 0000000..9699c76 --- /dev/null +++ b/templates/card/lib/game_internals/playing_card.dart @@ -0,0 +1,50 @@ +import 'dart:math'; + +import 'package:flutter/foundation.dart'; + +import 'card_suit.dart'; + +@immutable +class PlayingCard { + static final _random = Random(); + + final CardSuit suit; + + final int value; + + const PlayingCard(this.suit, this.value); + + factory PlayingCard.fromJson(Map json) { + return PlayingCard( + CardSuit.values + .singleWhere((e) => e.internalRepresentation == json['suit']), + json['value'], + ); + } + + factory PlayingCard.random([Random? random]) { + random ??= _random; + return PlayingCard( + CardSuit.values[random.nextInt(CardSuit.values.length)], + 2 + random.nextInt(9), + ); + } + + @override + int get hashCode => Object.hash(suit, value); + + @override + bool operator ==(Object other) { + return other is PlayingCard && other.suit == suit && other.value == value; + } + + Map toJson() => { + 'suit': suit.internalRepresentation, + 'value': value, + }; + + @override + String toString() { + return '$suit$value'; + } +} diff --git a/templates/card/lib/game_internals/score.dart b/templates/card/lib/game_internals/score.dart new file mode 100644 index 0000000..ec9a1d6 --- /dev/null +++ b/templates/card/lib/game_internals/score.dart @@ -0,0 +1,45 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Encapsulates a score and the arithmetic to compute it. +class Score { + final int score; + + final Duration duration; + + final int level; + + factory Score(int level, int difficulty, Duration duration) { + // The higher the difficulty, the higher the score. + var score = difficulty; + // The lower the time to beat the level, the higher the score. + score *= 10000 ~/ (duration.inSeconds.abs() + 1); + return Score._(score, duration, level); + } + + const Score._(this.score, this.duration, this.level); + + String get formattedTime { + final buf = StringBuffer(); + if (duration.inHours > 0) { + buf.write('${duration.inHours}'); + buf.write(':'); + } + final minutes = duration.inMinutes % Duration.minutesPerHour; + if (minutes > 9) { + buf.write('$minutes'); + } else { + buf.write('0'); + buf.write('$minutes'); + } + buf.write(':'); + buf.write((duration.inSeconds % Duration.secondsPerMinute) + .toString() + .padLeft(2, '0')); + return buf.toString(); + } + + @override + String toString() => 'Score<$score,$formattedTime,$level>'; +} diff --git a/templates/card/lib/main.dart b/templates/card/lib/main.dart new file mode 100644 index 0000000..9c2be0b --- /dev/null +++ b/templates/card/lib/main.dart @@ -0,0 +1,107 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:developer' as dev; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:logging/logging.dart'; +import 'package:provider/provider.dart'; + +import 'app_lifecycle/app_lifecycle.dart'; +import 'audio/audio_controller.dart'; +import 'player_progress/player_progress.dart'; +import 'router.dart'; +import 'settings/settings.dart'; +import 'style/palette.dart'; + +void main() async { + // Basic logging setup. + Logger.root.level = kDebugMode ? Level.FINE : Level.INFO; + Logger.root.onRecord.listen((record) { + dev.log( + record.message, + time: record.time, + level: record.level.value, + name: record.loggerName, + ); + }); + + WidgetsFlutterBinding.ensureInitialized(); + // Put game into full screen mode on mobile devices. + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + // Lock the game to portrait mode on mobile devices. + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return AppLifecycleObserver( + child: MultiProvider( + // This is where you add objects that you want to have available + // throughout your game. + // + // Every widget in the game can access these objects by calling + // `context.watch()` or `context.read()`. + // See `lib/main_menu/main_menu_screen.dart` for example usage. + providers: [ + Provider(create: (context) => SettingsController()), + Provider(create: (context) => Palette()), + ChangeNotifierProvider(create: (context) => PlayerProgress()), + // Set up audio. + ProxyProvider2( + create: (context) => AudioController(), + update: (context, lifecycleNotifier, settings, audio) { + audio!.attachDependencies(lifecycleNotifier, settings); + return audio; + }, + dispose: (context, audio) => audio.dispose(), + // Ensures that music starts immediately. + lazy: false, + ), + ], + child: Builder(builder: (context) { + final palette = context.watch(); + + return MaterialApp.router( + title: 'My Flutter Game', + theme: ThemeData.from( + colorScheme: ColorScheme.fromSeed( + seedColor: palette.darkPen, + background: palette.backgroundMain, + ), + textTheme: TextTheme( + bodyMedium: TextStyle(color: palette.ink), + ), + useMaterial3: true, + ).copyWith( + // Make buttons more fun. + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + textStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ), + ), + ), + routeInformationProvider: router.routeInformationProvider, + routeInformationParser: router.routeInformationParser, + routerDelegate: router.routerDelegate, + ); + }), + ), + ); + } +} diff --git a/templates/card/lib/main_menu/main_menu_screen.dart b/templates/card/lib/main_menu/main_menu_screen.dart new file mode 100644 index 0000000..3352384 --- /dev/null +++ b/templates/card/lib/main_menu/main_menu_screen.dart @@ -0,0 +1,80 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../audio/audio_controller.dart'; +import '../audio/sounds.dart'; +import '../settings/settings.dart'; +import '../style/my_button.dart'; +import '../style/palette.dart'; +import '../style/responsive_screen.dart'; + +class MainMenuScreen extends StatelessWidget { + const MainMenuScreen({super.key}); + + @override + Widget build(BuildContext context) { + final palette = context.watch(); + final settingsController = context.watch(); + final audioController = context.watch(); + + return Scaffold( + backgroundColor: palette.backgroundMain, + body: ResponsiveScreen( + squarishMainArea: Center( + child: Transform.rotate( + angle: -0.1, + child: const Text( + 'Drag&Drop Cards!', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Permanent Marker', + fontSize: 55, + height: 1, + ), + ), + ), + ), + rectangularMenuArea: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + MyButton( + onPressed: () { + audioController.playSfx(SfxType.buttonTap); + GoRouter.of(context).go('/play'); + }, + child: const Text('Play'), + ), + _gap, + MyButton( + onPressed: () => GoRouter.of(context).push('/settings'), + child: const Text('Settings'), + ), + _gap, + Padding( + padding: const EdgeInsets.only(top: 32), + child: ValueListenableBuilder( + valueListenable: settingsController.audioOn, + builder: (context, audioOn, child) { + return IconButton( + onPressed: () => settingsController.toggleAudioOn(), + icon: Icon(audioOn ? Icons.volume_up : Icons.volume_off), + ); + }, + ), + ), + _gap, + const Text('Music by Mr Smith'), + _gap, + ], + ), + ), + ); + } + + static const _gap = SizedBox(height: 10); +} diff --git a/templates/card/lib/play_session/board_widget.dart b/templates/card/lib/play_session/board_widget.dart new file mode 100644 index 0000000..8543644 --- /dev/null +++ b/templates/card/lib/play_session/board_widget.dart @@ -0,0 +1,46 @@ +// Copyright 2023, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../game_internals/board_state.dart'; +import 'player_hand_widget.dart'; +import 'playing_area_widget.dart'; + +/// This widget defines the game UI itself, without things like the settings +/// button or the back button. +class BoardWidget extends StatefulWidget { + const BoardWidget({super.key}); + + @override + State createState() => _BoardWidgetState(); +} + +class _BoardWidgetState extends State { + @override + Widget build(BuildContext context) { + final boardState = context.watch(); + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Wrap( + alignment: WrapAlignment.center, + spacing: 20, + runSpacing: 20, + children: [ + PlayingAreaWidget(boardState.areaOne), + PlayingAreaWidget(boardState.areaTwo), + ], + ), + ), + PlayerHandWidget(), + ], + ); + } +} diff --git a/templates/card/lib/play_session/play_session_screen.dart b/templates/card/lib/play_session/play_session_screen.dart new file mode 100644 index 0000000..b89a39f --- /dev/null +++ b/templates/card/lib/play_session/play_session_screen.dart @@ -0,0 +1,150 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart' hide Level; +import 'package:provider/provider.dart'; + +import '../audio/audio_controller.dart'; +import '../audio/sounds.dart'; +import '../game_internals/board_state.dart'; +import '../game_internals/score.dart'; +import '../style/confetti.dart'; +import '../style/my_button.dart'; +import '../style/palette.dart'; +import 'board_widget.dart'; + +/// This widget defines the entirety of the screen that the player sees when +/// they are playing a level. +/// +/// It is a stateful widget because it manages some state of its own, +/// such as whether the game is in a "celebration" state. +class PlaySessionScreen extends StatefulWidget { + const PlaySessionScreen({super.key}); + + @override + State createState() => _PlaySessionScreenState(); +} + +class _PlaySessionScreenState extends State { + static final _log = Logger('PlaySessionScreen'); + + static const _celebrationDuration = Duration(milliseconds: 2000); + + static const _preCelebrationDuration = Duration(milliseconds: 500); + + bool _duringCelebration = false; + + late DateTime _startOfPlay; + + late final BoardState _boardState; + + @override + Widget build(BuildContext context) { + final palette = context.watch(); + + return MultiProvider( + providers: [ + Provider.value(value: _boardState), + ], + child: IgnorePointer( + // Ignore all input during the celebration animation. + ignoring: _duringCelebration, + child: Scaffold( + backgroundColor: palette.backgroundPlaySession, + // The stack is how you layer widgets on top of each other. + // Here, it is used to overlay the winning confetti animation on top + // of the game. + body: Stack( + children: [ + // This is the main layout of the play session screen, + // with a settings button at top, the actual play area + // in the middle, and a back button at the bottom. + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Align( + alignment: Alignment.centerRight, + child: InkResponse( + onTap: () => GoRouter.of(context).push('/settings'), + child: Image.asset( + 'assets/images/settings.png', + semanticLabel: 'Settings', + ), + ), + ), + const Spacer(), + // The actual UI of the game. + BoardWidget(), + Text("Drag cards to the two areas above."), + const Spacer(), + Padding( + padding: const EdgeInsets.all(8.0), + child: MyButton( + onPressed: () => GoRouter.of(context).go('/'), + child: const Text('Back'), + ), + ), + ], + ), + SizedBox.expand( + child: Visibility( + visible: _duringCelebration, + child: IgnorePointer( + child: Confetti( + isStopped: !_duringCelebration, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + @override + void dispose() { + _boardState.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + _startOfPlay = DateTime.now(); + _boardState = BoardState(onWin: _playerWon); + } + + Future _playerWon() async { + _log.info('Player won'); + + // TODO: replace with some meaningful score for the card game + final score = Score(1, 1, DateTime.now().difference(_startOfPlay)); + + // final playerProgress = context.read(); + // playerProgress.setLevelReached(widget.level.number); + + // Let the player see the game just after winning for a bit. + await Future.delayed(_preCelebrationDuration); + if (!mounted) return; + + setState(() { + _duringCelebration = true; + }); + + final audioController = context.read(); + audioController.playSfx(SfxType.congrats); + + /// Give the player some time to see the celebration animation. + await Future.delayed(_celebrationDuration); + if (!mounted) return; + + GoRouter.of(context).go('/play/won', extra: {'score': score}); + } +} diff --git a/templates/card/lib/play_session/player_hand_widget.dart b/templates/card/lib/play_session/player_hand_widget.dart new file mode 100644 index 0000000..6ed598c --- /dev/null +++ b/templates/card/lib/play_session/player_hand_widget.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../game_internals/board_state.dart'; +import 'playing_card_widget.dart'; + +class PlayerHandWidget extends StatelessWidget { + const PlayerHandWidget({super.key}); + + @override + Widget build(BuildContext context) { + final boardState = context.watch(); + + return Padding( + padding: const EdgeInsets.all(10), + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: PlayingCardWidget.height), + child: ListenableBuilder( + // Make sure we rebuild every time there's an update + // to the player's hand. + listenable: boardState.player, + builder: (context, child) { + return Wrap( + alignment: WrapAlignment.center, + spacing: 10, + runSpacing: 10, + children: [ + ...boardState.player.hand.map((card) => + PlayingCardWidget(card, player: boardState.player)), + ], + ); + }, + ), + ), + ); + } +} diff --git a/templates/card/lib/play_session/playing_area_widget.dart b/templates/card/lib/play_session/playing_area_widget.dart new file mode 100644 index 0000000..18ad130 --- /dev/null +++ b/templates/card/lib/play_session/playing_area_widget.dart @@ -0,0 +1,121 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../audio/audio_controller.dart'; +import '../audio/sounds.dart'; +import '../game_internals/playing_area.dart'; +import '../game_internals/playing_card.dart'; +import '../style/palette.dart'; +import 'playing_card_widget.dart'; + +class PlayingAreaWidget extends StatefulWidget { + final PlayingArea area; + + const PlayingAreaWidget(this.area, {super.key}); + + @override + State createState() => _PlayingAreaWidgetState(); +} + +class _PlayingAreaWidgetState extends State { + bool isHighlighted = false; + + @override + Widget build(BuildContext context) { + final palette = context.watch(); + + return LimitedBox( + maxHeight: 200, + child: AspectRatio( + aspectRatio: 1 / 1, + child: DragTarget( + builder: (context, candidateData, rejectedData) => SizedBox( + height: 100, + child: Material( + color: isHighlighted ? palette.accept : palette.trueWhite, + shape: CircleBorder(), + clipBehavior: Clip.hardEdge, + child: InkWell( + splashColor: palette.redPen, + onTap: _onAreaTap, + child: StreamBuilder( + // Rebuild the card stack whenever the area changes + // (either by a player action, or remotely). + stream: widget.area.allChanges, + builder: (context, child) => _CardStack(widget.area.cards), + ), + ), + ), + ), + onWillAccept: _onDragWillAccept, + onLeave: _onDragLeave, + onAccept: _onDragAccept, + ), + ), + ); + } + + void _onAreaTap() { + widget.area.removeFirstCard(); + + final audioController = context.read(); + audioController.playSfx(SfxType.huhsh); + } + + void _onDragAccept(PlayingCardDragData data) { + widget.area.acceptCard(data.card); + data.holder.removeCard(data.card); + setState(() => isHighlighted = false); + } + + void _onDragLeave(PlayingCardDragData? data) { + setState(() => isHighlighted = false); + } + + bool _onDragWillAccept(PlayingCardDragData? data) { + if (data == null) return false; + setState(() => isHighlighted = true); + return true; + } +} + +class _CardStack extends StatelessWidget { + static const int _maxCards = 6; + + static const _leftOffset = 10.0; + + static const _topOffset = 5.0; + + static const double _maxWidth = + _maxCards * _leftOffset + PlayingCardWidget.width; + + static const _maxHeight = _maxCards * _topOffset + PlayingCardWidget.height; + + final List cards; + + const _CardStack(this.cards); + + @override + Widget build(BuildContext context) { + return Center( + child: SizedBox( + width: _maxWidth, + height: _maxHeight, + child: Stack( + children: [ + for (var i = max(0, cards.length - _maxCards); + i < cards.length; + i++) + Positioned( + top: i * _topOffset, + left: i * _leftOffset, + child: PlayingCardWidget(cards[i]), + ), + ], + ), + ), + ); + } +} diff --git a/templates/card/lib/play_session/playing_card_widget.dart b/templates/card/lib/play_session/playing_card_widget.dart new file mode 100644 index 0000000..61e1f50 --- /dev/null +++ b/templates/card/lib/play_session/playing_card_widget.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../audio/audio_controller.dart'; +import '../audio/sounds.dart'; +import '../game_internals/card_suit.dart'; +import '../game_internals/player.dart'; +import '../game_internals/playing_card.dart'; +import '../style/palette.dart'; + +class PlayingCardWidget extends StatelessWidget { + // A standard playing card is 57.1mm x 88.9mm. + static const double width = 57.1; + + static const double height = 88.9; + + final PlayingCard card; + + final Player? player; + + const PlayingCardWidget(this.card, {this.player, super.key}); + + @override + Widget build(BuildContext context) { + final palette = context.watch(); + final textColor = + card.suit.color == CardSuitColor.red ? palette.redPen : palette.ink; + + final cardWidget = DefaultTextStyle( + style: Theme.of(context).textTheme.bodyMedium!.apply(color: textColor), + child: Container( + width: width, + height: height, + decoration: BoxDecoration( + color: palette.trueWhite, + border: Border.all(color: palette.ink), + borderRadius: BorderRadius.circular(5), + ), + child: Center( + child: Text('${card.suit.asCharacter}\n${card.value}', + textAlign: TextAlign.center), + ), + ), + ); + + /// Cards that aren't in a player's hand are not draggable. + if (player == null) return cardWidget; + + return Draggable( + feedback: Transform.rotate( + angle: 0.1, + child: cardWidget, + ), + data: PlayingCardDragData(card, player!), + childWhenDragging: Opacity( + opacity: 0.5, + child: cardWidget, + ), + onDragStarted: () { + final audioController = context.read(); + audioController.playSfx(SfxType.huhsh); + }, + onDragEnd: (details) { + final audioController = context.read(); + audioController.playSfx(SfxType.wssh); + }, + child: cardWidget, + ); + } +} + +@immutable +class PlayingCardDragData { + final PlayingCard card; + + final Player holder; + + const PlayingCardDragData(this.card, this.holder); +} diff --git a/templates/card/lib/player_progress/persistence/local_storage_player_progress_persistence.dart b/templates/card/lib/player_progress/persistence/local_storage_player_progress_persistence.dart new file mode 100644 index 0000000..66c0d35 --- /dev/null +++ b/templates/card/lib/player_progress/persistence/local_storage_player_progress_persistence.dart @@ -0,0 +1,26 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:shared_preferences/shared_preferences.dart'; + +import 'player_progress_persistence.dart'; + +/// An implementation of [PlayerProgressPersistence] that uses +/// `package:shared_preferences`. +class LocalStoragePlayerProgressPersistence extends PlayerProgressPersistence { + final Future instanceFuture = + SharedPreferences.getInstance(); + + @override + Future getHighestLevelReached() async { + final prefs = await instanceFuture; + return prefs.getInt('highestLevelReached') ?? 0; + } + + @override + Future saveHighestLevelReached(int level) async { + final prefs = await instanceFuture; + await prefs.setInt('highestLevelReached', level); + } +} diff --git a/templates/card/lib/player_progress/persistence/memory_player_progress_persistence.dart b/templates/card/lib/player_progress/persistence/memory_player_progress_persistence.dart new file mode 100644 index 0000000..0913705 --- /dev/null +++ b/templates/card/lib/player_progress/persistence/memory_player_progress_persistence.dart @@ -0,0 +1,23 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'player_progress_persistence.dart'; + +/// An in-memory implementation of [PlayerProgressPersistence]. +/// Useful for testing. +class MemoryOnlyPlayerProgressPersistence implements PlayerProgressPersistence { + int level = 0; + + @override + Future getHighestLevelReached() async { + await Future.delayed(const Duration(milliseconds: 500)); + return level; + } + + @override + Future saveHighestLevelReached(int level) async { + await Future.delayed(const Duration(milliseconds: 500)); + this.level = level; + } +} diff --git a/templates/card/lib/player_progress/persistence/player_progress_persistence.dart b/templates/card/lib/player_progress/persistence/player_progress_persistence.dart new file mode 100644 index 0000000..123027a --- /dev/null +++ b/templates/card/lib/player_progress/persistence/player_progress_persistence.dart @@ -0,0 +1,13 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// An interface of persistence stores for the player's progress. +/// +/// Implementations can range from simple in-memory storage through +/// local preferences to cloud saves. +abstract class PlayerProgressPersistence { + Future getHighestLevelReached(); + + Future saveHighestLevelReached(int level); +} diff --git a/templates/card/lib/player_progress/player_progress.dart b/templates/card/lib/player_progress/player_progress.dart new file mode 100644 index 0000000..868d115 --- /dev/null +++ b/templates/card/lib/player_progress/player_progress.dart @@ -0,0 +1,64 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import 'persistence/local_storage_player_progress_persistence.dart'; +import 'persistence/player_progress_persistence.dart'; + +/// Encapsulates the player's progress. +class PlayerProgress extends ChangeNotifier { + static const maxHighestScoresPerPlayer = 10; + + /// By default, settings are persisted using + /// [LocalStoragePlayerProgressPersistence] (i.e. NSUserDefaults on iOS, + /// SharedPreferences on Android or local storage on the web). + final PlayerProgressPersistence _store; + + int _highestLevelReached = 0; + + /// Creates an instance of [PlayerProgress] backed by an injected + /// persistence [store]. + PlayerProgress({PlayerProgressPersistence? store}) + : _store = store ?? LocalStoragePlayerProgressPersistence() { + _getLatestFromStore(); + } + + /// The highest level that the player has reached so far. + int get highestLevelReached => _highestLevelReached; + + /// Resets the player's progress so it's like if they just started + /// playing the game for the first time. + void reset() { + _highestLevelReached = 0; + notifyListeners(); + _store.saveHighestLevelReached(_highestLevelReached); + } + + /// Registers [level] as reached. + /// + /// If this is higher than [highestLevelReached], it will update that + /// value and save it to the injected persistence store. + void setLevelReached(int level) { + if (level > _highestLevelReached) { + _highestLevelReached = level; + notifyListeners(); + + unawaited(_store.saveHighestLevelReached(level)); + } + } + + /// Fetches the latest data from the backing persistence store. + Future _getLatestFromStore() async { + final level = await _store.getHighestLevelReached(); + if (level > _highestLevelReached) { + _highestLevelReached = level; + notifyListeners(); + } else if (level < _highestLevelReached) { + await _store.saveHighestLevelReached(_highestLevelReached); + } + } +} diff --git a/templates/card/lib/router.dart b/templates/card/lib/router.dart new file mode 100644 index 0000000..14ddb47 --- /dev/null +++ b/templates/card/lib/router.dart @@ -0,0 +1,71 @@ +// Copyright 2023, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import 'game_internals/score.dart'; +import 'main_menu/main_menu_screen.dart'; +import 'play_session/play_session_screen.dart'; +import 'settings/settings_screen.dart'; +import 'style/my_transition.dart'; +import 'style/palette.dart'; +import 'win_game/win_game_screen.dart'; + +/// The router describes the game's navigational hierarchy, from the main +/// screen through settings screens all the way to each individual level. +final router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const MainMenuScreen(key: Key('main menu')), + routes: [ + GoRoute( + path: 'play', + pageBuilder: (context, state) => buildMyTransition( + key: ValueKey('play'), + color: context.watch().backgroundPlaySession, + child: const PlaySessionScreen( + key: Key('level selection'), + ), + ), + routes: [ + GoRoute( + path: 'won', + redirect: (context, state) { + if (state.extra == null) { + // Trying to navigate to a win screen without any data. + // Possibly by using the browser's back button. + return '/'; + } + + // Otherwise, do not redirect. + return null; + }, + pageBuilder: (context, state) { + final map = state.extra! as Map; + final score = map['score'] as Score; + + return buildMyTransition( + key: ValueKey('won'), + color: context.watch().backgroundPlaySession, + child: WinGameScreen( + score: score, + key: const Key('win game'), + ), + ); + }, + ) + ], + ), + GoRoute( + path: 'settings', + builder: (context, state) => + const SettingsScreen(key: Key('settings')), + ), + ], + ), + ], +); diff --git a/templates/card/lib/settings/custom_name_dialog.dart b/templates/card/lib/settings/custom_name_dialog.dart new file mode 100644 index 0000000..59dbcbb --- /dev/null +++ b/templates/card/lib/settings/custom_name_dialog.dart @@ -0,0 +1,76 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +import 'settings.dart'; + +void showCustomNameDialog(BuildContext context) { + showGeneralDialog( + context: context, + pageBuilder: (context, animation, secondaryAnimation) => + CustomNameDialog(animation: animation)); +} + +class CustomNameDialog extends StatefulWidget { + final Animation animation; + + const CustomNameDialog({required this.animation, super.key}); + + @override + State createState() => _CustomNameDialogState(); +} + +class _CustomNameDialogState extends State { + final TextEditingController _controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + return ScaleTransition( + scale: CurvedAnimation( + parent: widget.animation, + curve: Curves.easeOutCubic, + ), + child: SimpleDialog( + title: const Text('Change name'), + children: [ + TextField( + controller: _controller, + autofocus: true, + maxLength: 12, + maxLengthEnforcement: MaxLengthEnforcement.enforced, + textAlign: TextAlign.center, + textCapitalization: TextCapitalization.words, + textInputAction: TextInputAction.done, + onChanged: (value) { + context.read().setPlayerName(value); + }, + onSubmitted: (value) { + // Player tapped 'Submit'/'Done' on their keyboard. + Navigator.pop(context); + }, + ), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + _controller.text = context.read().playerName.value; + } +} diff --git a/templates/card/lib/settings/persistence/local_storage_settings_persistence.dart b/templates/card/lib/settings/persistence/local_storage_settings_persistence.dart new file mode 100644 index 0000000..41c56aa --- /dev/null +++ b/templates/card/lib/settings/persistence/local_storage_settings_persistence.dart @@ -0,0 +1,62 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:shared_preferences/shared_preferences.dart'; + +import 'settings_persistence.dart'; + +/// An implementation of [SettingsPersistence] that uses +/// `package:shared_preferences`. +class LocalStorageSettingsPersistence extends SettingsPersistence { + final Future instanceFuture = + SharedPreferences.getInstance(); + + @override + Future getAudioOn({required bool defaultValue}) async { + final prefs = await instanceFuture; + return prefs.getBool('mute') ?? defaultValue; + } + + @override + Future getMusicOn({required bool defaultValue}) async { + final prefs = await instanceFuture; + return prefs.getBool('musicOn') ?? defaultValue; + } + + @override + Future getPlayerName() async { + final prefs = await instanceFuture; + return prefs.getString('playerName') ?? 'Player'; + } + + @override + Future getSoundsOn({required bool defaultValue}) async { + final prefs = await instanceFuture; + return prefs.getBool('soundsOn') ?? defaultValue; + } + + @override + Future saveAudioOn(bool value) async { + final prefs = await instanceFuture; + await prefs.setBool('mute', value); + } + + @override + Future saveMusicOn(bool value) async { + final prefs = await instanceFuture; + await prefs.setBool('musicOn', value); + } + + @override + Future savePlayerName(String value) async { + final prefs = await instanceFuture; + await prefs.setString('playerName', value); + } + + @override + Future saveSoundsOn(bool value) async { + final prefs = await instanceFuture; + await prefs.setBool('soundsOn', value); + } +} diff --git a/templates/card/lib/settings/persistence/memory_settings_persistence.dart b/templates/card/lib/settings/persistence/memory_settings_persistence.dart new file mode 100644 index 0000000..14a9df7 --- /dev/null +++ b/templates/card/lib/settings/persistence/memory_settings_persistence.dart @@ -0,0 +1,41 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'settings_persistence.dart'; + +/// An in-memory implementation of [SettingsPersistence]. +/// Useful for testing. +class MemoryOnlySettingsPersistence implements SettingsPersistence { + bool musicOn = true; + + bool soundsOn = true; + + bool audioOn = true; + + String playerName = 'Player'; + + @override + Future getAudioOn({required bool defaultValue}) async => audioOn; + + @override + Future getMusicOn({required bool defaultValue}) async => musicOn; + + @override + Future getPlayerName() async => playerName; + + @override + Future getSoundsOn({required bool defaultValue}) async => soundsOn; + + @override + Future saveAudioOn(bool value) async => audioOn = value; + + @override + Future saveMusicOn(bool value) async => musicOn = value; + + @override + Future savePlayerName(String value) async => playerName = value; + + @override + Future saveSoundsOn(bool value) async => soundsOn = value; +} diff --git a/templates/card/lib/settings/persistence/settings_persistence.dart b/templates/card/lib/settings/persistence/settings_persistence.dart new file mode 100644 index 0000000..af93abf --- /dev/null +++ b/templates/card/lib/settings/persistence/settings_persistence.dart @@ -0,0 +1,25 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// An interface of persistence stores for settings. +/// +/// Implementations can range from simple in-memory storage through +/// local preferences to cloud-based solutions. +abstract class SettingsPersistence { + Future getAudioOn({required bool defaultValue}); + + Future getMusicOn({required bool defaultValue}); + + Future getPlayerName(); + + Future getSoundsOn({required bool defaultValue}); + + Future saveAudioOn(bool value); + + Future saveMusicOn(bool value); + + Future savePlayerName(String value); + + Future saveSoundsOn(bool value); +} diff --git a/templates/card/lib/settings/settings.dart b/templates/card/lib/settings/settings.dart new file mode 100644 index 0000000..4b9f93d --- /dev/null +++ b/templates/card/lib/settings/settings.dart @@ -0,0 +1,91 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; + +import 'persistence/local_storage_settings_persistence.dart'; +import 'persistence/settings_persistence.dart'; + +/// An class that holds settings like [playerName] or [musicOn], +/// and saves them to an injected persistence store. +class SettingsController { + static final _log = Logger('SettingsController'); + + /// The persistence store that is used to save settings. + final SettingsPersistence _store; + + /// Whether or not the audio is on at all. This overrides both music + /// and sounds (sfx). + /// + /// This is an important feature especially on mobile, where players + /// expect to be able to quickly mute all the audio. Having this as + /// a separate flag (as opposed to some kind of {off, sound, everything} + /// enum) means that the player will not lose their [soundsOn] and + /// [musicOn] preferences when they temporarily mute the game. + ValueNotifier audioOn = ValueNotifier(true); + + /// The player's name. Used for things like high score lists. + ValueNotifier playerName = ValueNotifier('Player'); + + /// Whether or not the sound effects (sfx) are on. + ValueNotifier soundsOn = ValueNotifier(true); + + /// Whether or not the music is on. + ValueNotifier musicOn = ValueNotifier(true); + + /// Creates a new instance of [SettingsController] backed by [store]. + /// + /// By default, settings are persisted using [LocalStorageSettingsPersistence] + /// (i.e. NSUserDefaults on iOS, SharedPreferences on Android or + /// local storage on the web). + SettingsController({SettingsPersistence? store}) + : _store = store ?? LocalStorageSettingsPersistence() { + _loadStateFromPersistence(); + } + + void setPlayerName(String name) { + playerName.value = name; + _store.savePlayerName(playerName.value); + } + + void toggleAudioOn() { + audioOn.value = !audioOn.value; + _store.saveAudioOn(audioOn.value); + } + + void toggleMusicOn() { + musicOn.value = !musicOn.value; + _store.saveMusicOn(musicOn.value); + } + + void toggleSoundsOn() { + soundsOn.value = !soundsOn.value; + _store.saveSoundsOn(soundsOn.value); + } + + /// Asynchronously loads values from the injected persistence store. + Future _loadStateFromPersistence() async { + final loadedValues = await Future.wait([ + _store.getAudioOn(defaultValue: true).then((value) { + if (kIsWeb) { + // On the web, sound can only start after user interaction, so + // we start muted there on every game start. + return audioOn.value = false; + } + // On other platforms, we can use the persisted value. + return audioOn.value = value; + }), + _store + .getSoundsOn(defaultValue: true) + .then((value) => soundsOn.value = value), + _store + .getMusicOn(defaultValue: true) + .then((value) => musicOn.value = value), + _store.getPlayerName().then((value) => playerName.value = value), + ]); + + _log.fine(() => 'Loaded settings: $loadedValues'); + } +} diff --git a/templates/card/lib/settings/settings_screen.dart b/templates/card/lib/settings/settings_screen.dart new file mode 100644 index 0000000..c62142a --- /dev/null +++ b/templates/card/lib/settings/settings_screen.dart @@ -0,0 +1,164 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../player_progress/player_progress.dart'; +import '../style/my_button.dart'; +import '../style/palette.dart'; +import '../style/responsive_screen.dart'; +import 'custom_name_dialog.dart'; +import 'settings.dart'; + +class SettingsScreen extends StatelessWidget { + const SettingsScreen({super.key}); + + static const _gap = SizedBox(height: 60); + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + final palette = context.watch(); + + return Scaffold( + backgroundColor: palette.backgroundSettings, + body: ResponsiveScreen( + squarishMainArea: ListView( + children: [ + _gap, + const Text( + 'Settings', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Permanent Marker', + fontSize: 55, + height: 1, + ), + ), + _gap, + const _NameChangeLine( + 'Name', + ), + ValueListenableBuilder( + valueListenable: settings.soundsOn, + builder: (context, soundsOn, child) => _SettingsLine( + 'Sound FX', + Icon(soundsOn ? Icons.graphic_eq : Icons.volume_off), + onSelected: () => settings.toggleSoundsOn(), + ), + ), + ValueListenableBuilder( + valueListenable: settings.musicOn, + builder: (context, musicOn, child) => _SettingsLine( + 'Music', + Icon(musicOn ? Icons.music_note : Icons.music_off), + onSelected: () => settings.toggleMusicOn(), + ), + ), + _SettingsLine( + 'Reset progress', + const Icon(Icons.delete), + onSelected: () { + context.read().reset(); + + final messenger = ScaffoldMessenger.of(context); + messenger.showSnackBar( + const SnackBar( + content: Text('Player progress has been reset.')), + ); + }, + ), + _gap, + ], + ), + rectangularMenuArea: MyButton( + onPressed: () { + GoRouter.of(context).pop(); + }, + child: const Text('Back'), + ), + ), + ); + } +} + +class _NameChangeLine extends StatelessWidget { + final String title; + + const _NameChangeLine(this.title); + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + + return InkResponse( + highlightShape: BoxShape.rectangle, + onTap: () => showCustomNameDialog(context), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(title, + style: const TextStyle( + fontFamily: 'Permanent Marker', + fontSize: 30, + )), + const Spacer(), + ValueListenableBuilder( + valueListenable: settings.playerName, + builder: (context, name, child) => Text( + '‘$name’', + style: const TextStyle( + fontFamily: 'Permanent Marker', + fontSize: 30, + ), + ), + ), + ], + ), + ), + ); + } +} + +class _SettingsLine extends StatelessWidget { + final String title; + + final Widget icon; + + final VoidCallback? onSelected; + + const _SettingsLine(this.title, this.icon, {this.onSelected}); + + @override + Widget build(BuildContext context) { + return InkResponse( + highlightShape: BoxShape.rectangle, + onTap: onSelected, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontFamily: 'Permanent Marker', + fontSize: 30, + ), + ), + ), + icon, + ], + ), + ), + ); + } +} diff --git a/templates/card/lib/style/confetti.dart b/templates/card/lib/style/confetti.dart new file mode 100644 index 0000000..b835224 --- /dev/null +++ b/templates/card/lib/style/confetti.dart @@ -0,0 +1,234 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:collection'; +import 'dart:math'; + +import 'package:flutter/widgets.dart'; + +/// Shows a confetti (celebratory) animation: paper snippings falling down. +/// +/// The widget fills the available space (like [SizedBox.expand] would). +/// +/// When [isStopped] is `true`, the animation will not run. This is useful +/// when the widget is not visible yet, for example. Provide [colors] +/// to make the animation look good in context. +/// +/// This is a partial port of this CodePen by Hemn Chawroka: +/// https://codepen.io/iprodev/pen/azpWBr +class Confetti extends StatefulWidget { + static const _defaultColors = [ + Color(0xffd10841), + Color(0xff1d75fb), + Color(0xff0050bc), + Color(0xffa2dcc7), + ]; + + final bool isStopped; + + final List colors; + + const Confetti({ + this.colors = _defaultColors, + this.isStopped = false, + super.key, + }); + + @override + State createState() => _ConfettiState(); +} + +class ConfettiPainter extends CustomPainter { + final defaultPaint = Paint(); + + final int snippingsCount = 200; + + late final List<_PaperSnipping> _snippings; + + Size? _size; + + DateTime _lastTime = DateTime.now(); + + final UnmodifiableListView colors; + + ConfettiPainter( + {required Listenable animation, required Iterable colors}) + : colors = UnmodifiableListView(colors), + super(repaint: animation); + + @override + void paint(Canvas canvas, Size size) { + if (_size == null) { + // First time we have a size. + _snippings = List.generate( + snippingsCount, + (i) => _PaperSnipping( + frontColor: colors[i % colors.length], + bounds: size, + )); + } + + final didResize = _size != null && _size != size; + final now = DateTime.now(); + final dt = now.difference(_lastTime); + for (final snipping in _snippings) { + if (didResize) { + snipping.updateBounds(size); + } + snipping.update(dt.inMilliseconds / 1000); + snipping.draw(canvas); + } + + _size = size; + _lastTime = now; + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} + +class _ConfettiState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: ConfettiPainter( + colors: widget.colors, + animation: _controller, + ), + willChange: true, + child: const SizedBox.expand(), + ); + } + + @override + void didUpdateWidget(covariant Confetti oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.isStopped && !widget.isStopped) { + _controller.repeat(); + } else if (!oldWidget.isStopped && widget.isStopped) { + _controller.stop(canceled: false); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + _controller = AnimationController( + // We don't really care about the duration, since we're going to + // use the controller on loop anyway. + duration: const Duration(seconds: 1), + vsync: this, + ); + + if (!widget.isStopped) { + _controller.repeat(); + } + } +} + +class _PaperSnipping { + static final Random _random = Random(); + + static const degToRad = pi / 180; + + static const backSideBlend = Color(0x70EEEEEE); + + Size _bounds; + + late final _Vector position = _Vector( + _random.nextDouble() * _bounds.width, + _random.nextDouble() * _bounds.height, + ); + + final double rotationSpeed = 800 + _random.nextDouble() * 600; + + final double angle = _random.nextDouble() * 360 * degToRad; + + double rotation = _random.nextDouble() * 360 * degToRad; + + double cosA = 1.0; + + final double size = 7.0; + + final double oscillationSpeed = 0.5 + _random.nextDouble() * 1.5; + + final double xSpeed = 40; + + final double ySpeed = 50 + _random.nextDouble() * 60; + + late List<_Vector> corners = List.generate(4, (i) { + final angle = this.angle + degToRad * (45 + i * 90); + return _Vector(cos(angle), sin(angle)); + }); + + double time = _random.nextDouble(); + + final Color frontColor; + + late final Color backColor = Color.alphaBlend(backSideBlend, frontColor); + + final paint = Paint()..style = PaintingStyle.fill; + + _PaperSnipping({ + required this.frontColor, + required Size bounds, + }) : _bounds = bounds; + + void draw(Canvas canvas) { + if (cosA > 0) { + paint.color = frontColor; + } else { + paint.color = backColor; + } + + final path = Path() + ..addPolygon( + List.generate( + 4, + (index) => Offset( + position.x + corners[index].x * size, + position.y + corners[index].y * size * cosA, + )), + true, + ); + canvas.drawPath(path, paint); + } + + void update(double dt) { + time += dt; + rotation += rotationSpeed * dt; + cosA = cos(degToRad * rotation); + position.x += cos(time * oscillationSpeed) * xSpeed * dt; + position.y += ySpeed * dt; + if (position.y > _bounds.height) { + // Move the snipping back to the top. + position.x = _random.nextDouble() * _bounds.width; + position.y = 0; + } + } + + void updateBounds(Size newBounds) { + if (!newBounds.contains(Offset(position.x, position.y))) { + position.x = _random.nextDouble() * newBounds.width; + position.y = _random.nextDouble() * newBounds.height; + } + _bounds = newBounds; + } +} + +class _Vector { + double x, y; + _Vector(this.x, this.y); +} diff --git a/templates/card/lib/style/my_button.dart b/templates/card/lib/style/my_button.dart new file mode 100644 index 0000000..d8dce67 --- /dev/null +++ b/templates/card/lib/style/my_button.dart @@ -0,0 +1,62 @@ +// Copyright 2023, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class MyButton extends StatefulWidget { + final Widget child; + + final VoidCallback? onPressed; + + const MyButton({super.key, required this.child, this.onPressed}); + + @override + State createState() => _MyButtonState(); +} + +class _MyButtonState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (event) { + _controller.repeat(); + }, + onExit: (event) { + _controller.stop(canceled: false); + }, + child: RotationTransition( + turns: _controller.drive(const _MySineTween(0.005)), + child: FilledButton( + onPressed: widget.onPressed, + child: widget.child, + ), + ), + ); + } +} + +class _MySineTween extends Animatable { + final double maxExtent; + + const _MySineTween(this.maxExtent); + + @override + double transform(double t) { + return sin(t * 2 * pi) * maxExtent; + } +} diff --git a/templates/card/lib/style/my_transition.dart b/templates/card/lib/style/my_transition.dart new file mode 100644 index 0000000..9c44d3d --- /dev/null +++ b/templates/card/lib/style/my_transition.dart @@ -0,0 +1,77 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +CustomTransitionPage buildMyTransition({ + required Widget child, + required Color color, + String? name, + Object? arguments, + String? restorationId, + LocalKey? key, +}) { + return CustomTransitionPage( + child: child, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return _MyReveal( + animation: animation, + color: color, + child: child, + ); + }, + key: key, + name: name, + arguments: arguments, + restorationId: restorationId, + transitionDuration: const Duration(milliseconds: 700), + ); +} + +class _MyReveal extends StatelessWidget { + final Widget child; + + final Animation animation; + + final Color color; + + final _slideTween = Tween(begin: const Offset(0, -1), end: Offset.zero); + + final _fadeTween = TweenSequence([ + TweenSequenceItem(tween: ConstantTween(0.0), weight: 1), + TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 1), + ]); + + _MyReveal({ + required this.child, + required this.animation, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return Stack( + fit: StackFit.expand, + children: [ + SlideTransition( + position: _slideTween.animate( + CurvedAnimation( + parent: animation, + curve: Curves.easeOutCubic, + reverseCurve: Curves.easeOutCubic, + ), + ), + child: Container( + color: color, + ), + ), + FadeTransition( + opacity: _fadeTween.animate(animation), + child: child, + ), + ], + ); + } +} diff --git a/templates/card/lib/style/palette.dart b/templates/card/lib/style/palette.dart new file mode 100644 index 0000000..b483cd2 --- /dev/null +++ b/templates/card/lib/style/palette.dart @@ -0,0 +1,38 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// A palette of colors to be used in the game. +/// +/// The reason we're not going with something like Material Design's +/// `Theme` is simply that this is simpler to work with and yet gives +/// us everything we need for a game. +/// +/// Games generally have more radical color palettes than apps. For example, +/// every level of a game can have radically different colors. +/// At the same time, games rarely support dark mode. +/// +/// Colors taken from this fun palette: +/// https://lospec.com/palette-list/crayola84 +/// +/// Colors here are implemented as getters so that hot reloading works. +/// In practice, we could just as easily implement the colors +/// as `static const`. But this way the palette is more malleable: +/// we could allow players to customize colors, for example, +/// or even get the colors from the network. +class Palette { + Color get pen => const Color(0xff1d75fb); + Color get darkPen => const Color(0xFF0050bc); + Color get redPen => const Color(0xFFd10841); + Color get inkFullOpacity => const Color(0xff352b42); + Color get ink => const Color(0xee352b42); + Color get accept => const Color(0xff15a44d); + Color get backgroundMain => const Color(0xffffd7ff); + Color get backgroundLevelSelection => const Color(0xffa2dcc7); + Color get backgroundPlaySession => const Color(0xffffebb5); + Color get background4 => const Color(0xffffffd1); + Color get backgroundSettings => const Color(0xffbfc8e3); + Color get trueWhite => const Color(0xffffffff); +} diff --git a/templates/card/lib/style/responsive_screen.dart b/templates/card/lib/style/responsive_screen.dart new file mode 100644 index 0000000..a6d7ebe --- /dev/null +++ b/templates/card/lib/style/responsive_screen.dart @@ -0,0 +1,121 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// A widget that makes it easy to create a screen with a square-ish +/// main area, a smaller menu area, and a small area for a message on top. +/// It works in both orientations on mobile- and tablet-sized screens. +class ResponsiveScreen extends StatelessWidget { + /// This is the "hero" of the screen. It's more or less square, and will + /// be placed in the visual "center" of the screen. + final Widget squarishMainArea; + + /// The second-largest area after [squarishMainArea]. It can be narrow + /// or wide. + final Widget rectangularMenuArea; + + /// An area reserved for some static text close to the top of the screen. + final Widget topMessageArea; + + const ResponsiveScreen({ + required this.squarishMainArea, + required this.rectangularMenuArea, + this.topMessageArea = const SizedBox.shrink(), + super.key, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + // This widget wants to fill the whole screen. + final size = constraints.biggest; + final padding = EdgeInsets.all(size.shortestSide / 30); + + if (size.height >= size.width) { + // "Portrait" / "mobile" mode. + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SafeArea( + bottom: false, + child: Padding( + padding: padding, + child: topMessageArea, + ), + ), + Expanded( + child: SafeArea( + top: false, + bottom: false, + minimum: padding, + child: squarishMainArea, + ), + ), + SafeArea( + top: false, + maintainBottomViewPadding: true, + child: Padding( + padding: padding, + child: Center( + child: rectangularMenuArea, + ), + ), + ), + ], + ); + } else { + // "Landscape" / "tablet" mode. + final isLarge = size.width > 900; + + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + flex: isLarge ? 7 : 5, + child: SafeArea( + right: false, + maintainBottomViewPadding: true, + minimum: padding, + child: squarishMainArea, + ), + ), + Expanded( + flex: 3, + child: Column( + children: [ + SafeArea( + bottom: false, + left: false, + maintainBottomViewPadding: true, + child: Padding( + padding: padding, + child: topMessageArea, + ), + ), + Expanded( + child: SafeArea( + top: false, + left: false, + maintainBottomViewPadding: true, + child: Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: padding, + child: rectangularMenuArea, + ), + ), + ), + ) + ], + ), + ), + ], + ); + } + }, + ); + } +} diff --git a/templates/card/lib/win_game/win_game_screen.dart b/templates/card/lib/win_game/win_game_screen.dart new file mode 100644 index 0000000..bc1f717 --- /dev/null +++ b/templates/card/lib/win_game/win_game_screen.dart @@ -0,0 +1,61 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../game_internals/score.dart'; +import '../style/my_button.dart'; +import '../style/palette.dart'; +import '../style/responsive_screen.dart'; + +class WinGameScreen extends StatelessWidget { + final Score score; + + const WinGameScreen({ + super.key, + required this.score, + }); + + @override + Widget build(BuildContext context) { + final palette = context.watch(); + + const gap = SizedBox(height: 10); + + return Scaffold( + backgroundColor: palette.backgroundPlaySession, + body: ResponsiveScreen( + squarishMainArea: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + gap, + const Center( + child: Text( + 'You won!', + style: TextStyle(fontFamily: 'Permanent Marker', fontSize: 50), + ), + ), + gap, + Center( + child: Text( + 'Score: ${score.score}\n' + 'Time: ${score.formattedTime}', + style: const TextStyle( + fontFamily: 'Permanent Marker', fontSize: 20), + ), + ), + ], + ), + rectangularMenuArea: MyButton( + onPressed: () { + GoRouter.of(context).go('/'); + }, + child: const Text('Continue'), + ), + ), + ); + } +} diff --git a/templates/card/pubspec.yaml b/templates/card/pubspec.yaml new file mode 100644 index 0000000..4f2f263 --- /dev/null +++ b/templates/card/pubspec.yaml @@ -0,0 +1,49 @@ +name: card +description: A game built in Flutter. + +# Prevent accidental publishing to pub.dev. +publish_to: 'none' + +version: 0.0.1+1 + +environment: + sdk: ^3.0.0 + +dependencies: + flutter: + sdk: flutter + + async: ^2.11.0 + audioplayers: ^5.2.0 + cupertino_icons: ^1.0.6 + go_router: ^12.0.1 + logging: ^1.2.0 + provider: ^6.0.5 + shared_preferences: ^2.2.2 + +dev_dependencies: + flutter_lints: ^3.0.0 + flutter_test: + sdk: flutter + flutter_launcher_icons: ^0.13.1 + test: ^1.24.3 + +flutter: + uses-material-design: true + + assets: + - assets/images/ + - assets/music/ + - assets/sfx/ + + fonts: + - family: Permanent Marker + fonts: + - asset: assets/fonts/Permanent_Marker/PermanentMarker-Regular.ttf + +flutter_launcher_icons: + android: true + ios: true + image_path: "assets/icons/icon.png" + adaptive_icon_background: "#FFFFFF" + adaptive_icon_foreground: "assets/icons/icon-adaptive-foreground.png" diff --git a/templates/card/test/smoke_test.dart b/templates/card/test/smoke_test.dart new file mode 100644 index 0000000..e5548c2 --- /dev/null +++ b/templates/card/test/smoke_test.dart @@ -0,0 +1,41 @@ +// Copyright 2022, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:card/main.dart'; +import 'package:card/play_session/playing_card_widget.dart'; + +void main() { + testWidgets('smoke test', (tester) async { + // Build our game and trigger a frame. + await tester.pumpWidget(MyApp()); + + // Verify that the 'Play' button is shown. + expect(find.text('Play'), findsOneWidget); + + // Verify that the 'Settings' button is shown. + expect(find.text('Settings'), findsOneWidget); + + // Go to 'Settings'. + await tester.tap(find.text('Settings')); + await tester.pumpAndSettle(); + expect(find.text('Music'), findsOneWidget); + + // Go back to main menu. + await tester.tap(find.text('Back')); + await tester.pumpAndSettle(); + + // Tap 'Play'. + await tester.tap(find.text('Play')); + await tester.pumpAndSettle(); + expect(find.byType(PlayingCardWidget), findsWidgets); + + // Tap 'Back'. + await tester.tap(find.text('Back')); + await tester.pumpAndSettle(); + + // Verify we're back on the homepage. + expect(find.text('Play'), findsOneWidget); + }); +} diff --git a/templates/card/web/favicon.png b/templates/card/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/templates/card/web/favicon.png differ diff --git a/templates/card/web/icons/Icon-192.png b/templates/card/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/templates/card/web/icons/Icon-192.png differ diff --git a/templates/card/web/icons/Icon-512.png b/templates/card/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/templates/card/web/icons/Icon-512.png differ diff --git a/templates/card/web/icons/Icon-maskable-192.png b/templates/card/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/templates/card/web/icons/Icon-maskable-192.png differ diff --git a/templates/card/web/icons/Icon-maskable-512.png b/templates/card/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/templates/card/web/icons/Icon-maskable-512.png differ diff --git a/templates/card/web/index.html b/templates/card/web/index.html new file mode 100644 index 0000000..d56a11b --- /dev/null +++ b/templates/card/web/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + card + + + + + + + + + + diff --git a/templates/card/web/manifest.json b/templates/card/web/manifest.json new file mode 100644 index 0000000..4867c6b --- /dev/null +++ b/templates/card/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "card", + "short_name": "card", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}