├── .github ├── FUNDING.yml └── workflows │ └── compile_apk.yml ├── .gitignore ├── .metadata ├── LICENSE.txt ├── README.md ├── README_zh.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle.kts │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── motion_ease_tune │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle.kts ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle.kts ├── assets ├── ComicRelief │ ├── ComicRelief.ttf │ └── OFL.txt ├── IzzyOnDroid.png ├── guide │ ├── listen-for-a-minute.png │ ├── same-volume-to-ears.png │ └── use-before-onset.png └── icon │ ├── github-mark-white.png │ ├── github-mark.png │ └── icon.png ├── fastlane └── metadata │ └── android │ └── en-US │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── main_view.jpg │ │ ├── user_guide_1.jpg │ │ ├── user_guide_2.jpg │ │ └── user_guide_3.jpg │ ├── short_description.txt │ └── title.txt ├── l10n.yaml ├── lib ├── features │ ├── guide.dart │ ├── home │ │ ├── home.dart │ │ ├── sine_wave.dart │ │ └── sound_player.dart │ └── settings │ │ ├── about_page.dart │ │ ├── language_settings.dart │ │ └── settings.dart ├── l10n │ ├── app_en.arb │ ├── app_localizations.dart │ ├── app_localizations_en.dart │ ├── app_localizations_zh.dart │ └── app_zh.arb ├── main.dart ├── providers │ ├── language_provider.dart │ └── theme_provider.dart └── theme.dart ├── pubspec.lock ├── pubspec.yaml ├── screenshots ├── main_view.jpg ├── user_guide_1.jpg ├── user_guide_2.jpg └── user_guide_3.jpg └── test └── widget_test.dart /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: bhznjns 2 | liberapay: BHznJNs -------------------------------------------------------------------------------- /.github/workflows/compile_apk.yml: -------------------------------------------------------------------------------- 1 | name: Compile APK 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | env: 8 | FLUTTER_VERSION: "3.29.3" 9 | APK_BUILD_DIR: "/tmp/build" 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-22.04 14 | outputs: 15 | version: ${{ steps.get_version.outputs.version }} 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Get version from pubspec.yaml 20 | id: get_version 21 | run: | 22 | VERSION=$(sed -n 's/^version: \([0-9]*\.[0-9]*\.[0-9]*\).*/\1/p' pubspec.yaml) 23 | echo "version=$VERSION" >> $GITHUB_OUTPUT 24 | 25 | build_apk: 26 | needs: build 27 | runs-on: ubuntu-22.04 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v4 32 | 33 | - name: Copy files to env.APK_BUILD_DIR 34 | run: | 35 | mkdir -p $APK_BUILD_DIR 36 | cp -r . $APK_BUILD_DIR 37 | 38 | - name: Set up JDK 21 39 | uses: actions/setup-java@v4 40 | with: 41 | distribution: 'temurin' 42 | java-version: '21' 43 | 44 | - name: Install Flutter 45 | uses: subosito/flutter-action@v2 46 | with: 47 | flutter-version: ${{ env.FLUTTER_VERSION }} 48 | 49 | - name: Dependencies 50 | working-directory: ${{ env.APK_BUILD_DIR }} 51 | run: flutter pub get 52 | 53 | - name: Decode Keystore 54 | working-directory: ${{ env.APK_BUILD_DIR }} 55 | run: | 56 | echo "${{ secrets.SIGNING_KEYSTORE }}" | base64 -d > android/app/my_app_release_key.jks 57 | shell: bash 58 | 59 | - name: Create Signing Properties 60 | working-directory: ${{ env.APK_BUILD_DIR }} 61 | run: | 62 | echo "storeFile=my_app_release_key.jks" > android/app/signing.properties 63 | echo "storePassword=${{ secrets.SIGNING_KEYSTORE_PASSWORD }}" >> android/app/signing.properties 64 | echo "keyAlias=${{ secrets.SIGNING_KEY_ALIAS }}" >> android/app/signing.properties 65 | echo "keyPassword=${{ secrets.SIGNING_KEY_PASSWORD }}" >> android/app/signing.properties 66 | shell: bash 67 | 68 | - name: Build APK 69 | working-directory: ${{ env.APK_BUILD_DIR }} 70 | run: flutter build apk --split-per-abi 71 | 72 | - name: Release 73 | uses: softprops/action-gh-release@v2 74 | with: 75 | token: ${{ secrets.GITHUB_TOKEN }} 76 | files: | 77 | ${{ env.APK_BUILD_DIR }}/build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk 78 | ${{ env.APK_BUILD_DIR }}/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk 79 | ${{ env.APK_BUILD_DIR }}/build/app/outputs/flutter-apk/app-x86_64-release.apk -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "ea121f8859e4b13e47a8f845e4586164519588bc" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: ea121f8859e4b13e47a8f845e4586164519588bc 17 | base_revision: ea121f8859e4b13e47a8f845e4586164519588bc 18 | - platform: android 19 | create_revision: ea121f8859e4b13e47a8f845e4586164519588bc 20 | base_revision: ea121f8859e4b13e47a8f845e4586164519588bc 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | MotionEaseTune Logo 4 |

MotionEaseTune

5 | 中文介绍 | 6 | Install | 7 | Feedback 8 |
9 |
10 |
11 | 12 | A simple application that can prevent you from motion sickness via 100Hz sound. 13 | 14 | 15 | 16 | ## Usage 17 | 18 | 1. Just [download](https://github.com/BHznJNs/MotionEaseTune/releases) and open this application 19 | 2. Use it before the onset of motion sickness symptoms 20 | 3. Make sure the volume is the same for both ears 21 | 4. Listen for about one minute 22 | 23 | That's it! 24 | 25 | ## Screenshots 26 | 27 | 28 | 29 | 30 | 31 | 32 | ## Reference 33 | 34 | [Just 1-min exposure to a pure tone at 100 Hz with daily exposable sound pressure levels may improve motion sickness](https://doi.org/10.1265/ehpm.24-00247) 35 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | MotionEaseTune Logo 4 |

MotionEaseTune

5 | 中文介绍 | 6 | 安装 | 7 | 反馈 8 |
9 |
10 |
11 | 12 | 一个通过 100Hz 声音来帮助你缓解晕车症状的简单应用。 13 | 14 | ## 用法 15 | 16 | 1. [下载](https://github.com/BHznJNs/MotionEaseTune/releases)打开这个应用 17 | 2. 在有晕车症状前使用这个应用 18 | 3. 确保双耳能听到相同大小的声音 19 | 4. 持续听一分钟 20 | 21 | 就这么简单! 22 | 23 | ## 参考文献 24 | 25 | [只需要聆听 1 分钟的特殊声音,就有可能预防晕车](https://doi.org/10.1265/ehpm.24-00247) 26 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | .cxx/ 9 | 10 | # Remember to never publicly share your keystore. 11 | # See https://flutter.dev/to/reference-keystore 12 | key.properties 13 | **/*.keystore 14 | **/*.jks 15 | -------------------------------------------------------------------------------- /android/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.util.Properties 2 | import java.io.FileInputStream 3 | 4 | val signingPropertiesFile = file("./signing.properties") 5 | val signingProperties = Properties() 6 | 7 | if (signingPropertiesFile.exists()) { 8 | signingProperties.load(FileInputStream(signingPropertiesFile)) 9 | println(">>> Loaded signing.properties") // Optional: for debugging during build 10 | } else { 11 | println(">>> signing.properties not found. Release build might not be signed correctly.") // Optional: warning 12 | } 13 | 14 | val storeFileProp = signingProperties.getProperty("storeFile") 15 | val storePasswordProp = signingProperties.getProperty("storePassword") 16 | val keyAliasProp = signingProperties.getProperty("keyAlias") 17 | val keyPasswordProp = signingProperties.getProperty("keyPassword") 18 | 19 | 20 | plugins { 21 | id("com.android.application") 22 | id("kotlin-android") 23 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 24 | id("dev.flutter.flutter-gradle-plugin") 25 | } 26 | 27 | android { 28 | namespace = "com.bhznjns.motion_ease_tune" 29 | compileSdk = flutter.compileSdkVersion 30 | ndkVersion = "27.0.12077973" 31 | 32 | compileOptions { 33 | sourceCompatibility = JavaVersion.VERSION_11 34 | targetCompatibility = JavaVersion.VERSION_11 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = JavaVersion.VERSION_11.toString() 39 | } 40 | 41 | defaultConfig { 42 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 43 | applicationId = "com.bhznjns.motion_ease_tune" 44 | // You can update the following values to match your application needs. 45 | // For more information, see: https://flutter.dev/to/review-gradle-config. 46 | minSdk = flutter.minSdkVersion 47 | targetSdk = flutter.targetSdkVersion 48 | versionCode = flutter.versionCode 49 | versionName = flutter.versionName 50 | } 51 | 52 | dependenciesInfo { 53 | // Disables dependency metadata when building APKs (for IzzyOnDroid/F-Droid) 54 | includeInApk = false 55 | // Disables dependency metadata when building Android App Bundles (for Google Play) 56 | includeInBundle = false 57 | } 58 | 59 | signingConfigs { 60 | create("release") { 61 | if (storeFileProp != null && storePasswordProp != null && keyAliasProp != null && keyPasswordProp != null) { 62 | storeFile = file(storeFileProp) 63 | storePassword = storePasswordProp 64 | keyAlias = keyAliasProp 65 | keyPassword = keyPasswordProp 66 | } else { 67 | println(">>> Incomplete signing properties. Release build may not be signed correctly.") // Optional: warning 68 | } 69 | } 70 | } 71 | 72 | buildTypes { 73 | release { 74 | // Signing with the debug keys for now, so `flutter run --release` works. 75 | // signingConfig = signingConfigs.getByName("debug") 76 | if (storeFileProp != null) { 77 | signingConfig = signingConfigs.getByName("release") 78 | } else { 79 | println(">>> Release build signing config not applied.") // Optional: warning 80 | } 81 | } 82 | } 83 | } 84 | 85 | flutter { 86 | source = "../.." 87 | } 88 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/motion_ease_tune/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.bhznjns.motion_ease_tune 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity : FlutterActivity() 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() 9 | rootProject.layout.buildDirectory.value(newBuildDir) 10 | 11 | subprojects { 12 | val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) 13 | project.layout.buildDirectory.value(newSubprojectBuildDir) 14 | } 15 | subprojects { 16 | project.evaluationDependsOn(":app") 17 | } 18 | 19 | tasks.register("clean") { 20 | delete(rootProject.layout.buildDirectory) 21 | } 22 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | val flutterSdkPath = run { 3 | val properties = java.util.Properties() 4 | file("local.properties").inputStream().use { properties.load(it) } 5 | val flutterSdkPath = properties.getProperty("flutter.sdk") 6 | require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } 7 | flutterSdkPath 8 | } 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id("dev.flutter.flutter-plugin-loader") version "1.0.0" 21 | id("com.android.application") version "8.7.0" apply false 22 | id("org.jetbrains.kotlin.android") version "1.8.22" apply false 23 | } 24 | 25 | include(":app") 26 | -------------------------------------------------------------------------------- /assets/ComicRelief/ComicRelief.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/assets/ComicRelief/ComicRelief.ttf -------------------------------------------------------------------------------- /assets/ComicRelief/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2013 The Comic Relief Project Authors (https://github.com/loudifier/Comic-Relief) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | https://openfontlicense.org 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /assets/IzzyOnDroid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/assets/IzzyOnDroid.png -------------------------------------------------------------------------------- /assets/guide/listen-for-a-minute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/assets/guide/listen-for-a-minute.png -------------------------------------------------------------------------------- /assets/guide/same-volume-to-ears.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/assets/guide/same-volume-to-ears.png -------------------------------------------------------------------------------- /assets/guide/use-before-onset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/assets/guide/use-before-onset.png -------------------------------------------------------------------------------- /assets/icon/github-mark-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/assets/icon/github-mark-white.png -------------------------------------------------------------------------------- /assets/icon/github-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/assets/icon/github-mark.png -------------------------------------------------------------------------------- /assets/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/assets/icon/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | A simple application that can prevent you from motion sickness via 100Hz sound. 2 | 3 | ## Usage 4 | 5 | 1. Just download and open this application 6 | 2. Use it before the onset of motion sickness symptoms 7 | 3. Make sure the volume is the same for both ears 8 | 4. Listen for about one minute 9 | 10 | That's it! 11 | 12 | ## Reference 13 | 14 | Just 1-min exposure to a pure tone at 100 Hz with daily exposable sound pressure levels may improve motion sickness 15 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/main_view.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/fastlane/metadata/android/en-US/images/phoneScreenshots/main_view.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/user_guide_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/fastlane/metadata/android/en-US/images/phoneScreenshots/user_guide_1.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/user_guide_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/fastlane/metadata/android/en-US/images/phoneScreenshots/user_guide_2.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/user_guide_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/fastlane/metadata/android/en-US/images/phoneScreenshots/user_guide_3.jpg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | A simple application that can prevent you from motion sickness via 100Hz sound. 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | MotionEaseTune -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/l10n 2 | synthetic-package: false 3 | template-arb-file: app_en.arb 4 | output-localization-file: app_localizations.dart 5 | -------------------------------------------------------------------------------- /lib/features/guide.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:motion_ease_tune/l10n/app_localizations.dart'; 3 | 4 | Future openGuideDialog(BuildContext context) async { 5 | return await showDialog( 6 | context: context, 7 | builder: (BuildContext context) { 8 | return UserGuideDialog(); 9 | } 10 | ); 11 | } 12 | 13 | class UserGuideDialog extends StatefulWidget { 14 | const UserGuideDialog({super.key}); 15 | 16 | @override 17 | State createState() => _UserGuideDialogState(); 18 | } 19 | class _UserGuideDialogState extends State { 20 | late PageController _pageController; 21 | final int _itemCount = 3; 22 | int _currentPageIndex = 0; 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | _pageController = PageController(initialPage: _currentPageIndex); 28 | } 29 | 30 | @override 31 | void dispose() { 32 | _pageController.dispose(); 33 | super.dispose(); 34 | } 35 | 36 | Widget page1Builder() { 37 | final l10n = AppLocalizations.of(context)!; 38 | return Column( 39 | children: [ 40 | Image.asset( 41 | 'assets/guide/use-before-onset.png', 42 | fit: BoxFit.contain, 43 | ), 44 | Padding( 45 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 46 | child: Column( 47 | crossAxisAlignment: CrossAxisAlignment.start, 48 | children: [ 49 | Text( 50 | l10n.useBeforeOnsetTitle, 51 | style: TextStyle(fontSize: 24), 52 | ), 53 | Text( 54 | l10n.useBeforeOnsetDescription, 55 | style: TextStyle(fontSize: 16), 56 | ), 57 | ], 58 | ), 59 | ), 60 | ], 61 | ); 62 | } 63 | 64 | @override 65 | Widget build(BuildContext context) { 66 | final l10n = AppLocalizations.of(context)!; 67 | final isLastPage = _currentPageIndex == _itemCount - 1; 68 | final pageView = PageView( 69 | scrollDirection: Axis.horizontal, 70 | controller: _pageController, 71 | onPageChanged: (index) => setState(() => _currentPageIndex = index), 72 | children: [ 73 | _UserGuidePage( 74 | image: 'assets/guide/use-before-onset.png', 75 | title: l10n.useBeforeOnsetTitle, 76 | description: l10n.useBeforeOnsetDescription, 77 | ), 78 | _UserGuidePage( 79 | image: 'assets/guide/same-volume-to-ears.png', 80 | title: l10n.sameVolumeToEarsTitle, 81 | description: l10n.sameVolumeToEarsDescription, 82 | ), 83 | _UserGuidePage( 84 | image: 'assets/guide/listen-for-a-minute.png', 85 | title: l10n.listenForAMinuteTitle, 86 | description: l10n.listenForAMinuteDescription, 87 | ), 88 | ], 89 | ); 90 | final nextButton = TextButton( 91 | child: Text(l10n.nextButton), 92 | onPressed: () { 93 | _pageController.nextPage( 94 | duration: Duration(milliseconds: 300), 95 | curve: Curves.easeInOut, 96 | ); 97 | }, 98 | ); 99 | final doneButton = TextButton( 100 | child: Text(l10n.doneButton), 101 | onPressed: () { 102 | Navigator.of(context).pop(); 103 | }, 104 | ); 105 | return Dialog.fullscreen( 106 | child: Scaffold( 107 | appBar: AppBar( 108 | title: Text(l10n.userGuideTitle), 109 | actions: [ 110 | isLastPage 111 | ? doneButton 112 | : nextButton, 113 | ], 114 | ), 115 | body: pageView, 116 | ) , 117 | ); 118 | } 119 | } 120 | 121 | class _UserGuidePage extends StatelessWidget { 122 | const _UserGuidePage({ 123 | required this.image, 124 | required this.title, 125 | required this.description, 126 | }); 127 | 128 | final String image; 129 | final String title; 130 | final String description; 131 | 132 | @override 133 | Widget build(BuildContext context) { 134 | final imageAsset = Image.asset( 135 | image, 136 | fit: BoxFit.contain, 137 | ); 138 | final titleWidget = Text( 139 | title, 140 | style: TextStyle(fontSize: 24), 141 | ); 142 | final descriptionWidget = Text( 143 | description, 144 | style: TextStyle(fontSize: 16), 145 | ); 146 | return LayoutBuilder( 147 | builder: (context, constraints) { 148 | final availableHeight = constraints.maxHeight; 149 | final availableWidth = constraints.maxWidth; 150 | final shouldOverlayText = availableHeight < ((availableWidth * (4 / 3)) + 72); 151 | if (shouldOverlayText) { 152 | return Stack( 153 | alignment: Alignment.bottomCenter, 154 | children: [ 155 | Positioned( 156 | top: 0, 157 | left: 0, 158 | right: 0, 159 | child: imageAsset, 160 | ), 161 | Container( 162 | width: double.infinity, 163 | color: Theme.of(context).cardColor.withAlpha(210), 164 | padding: const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 36), 165 | child: Column( 166 | crossAxisAlignment: CrossAxisAlignment.start, 167 | mainAxisSize: MainAxisSize.min, 168 | children: [ 169 | titleWidget, 170 | descriptionWidget, 171 | ], 172 | ), 173 | ), 174 | ], 175 | ); 176 | } else { 177 | return Column( 178 | children: [ 179 | imageAsset, 180 | Padding( 181 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 182 | child: Column( 183 | crossAxisAlignment: CrossAxisAlignment.start, 184 | children: [ 185 | titleWidget, 186 | descriptionWidget, 187 | ], 188 | ), 189 | ), 190 | ], 191 | ); 192 | } 193 | } 194 | ); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /lib/features/home/home.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; 5 | import 'package:motion_ease_tune/features/settings/settings.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | import 'package:real_volume/real_volume.dart'; 8 | import 'package:motion_ease_tune/l10n/app_localizations.dart'; 9 | import 'package:motion_ease_tune/features/guide.dart'; 10 | import 'package:motion_ease_tune/features/home/sine_wave.dart'; 11 | import 'package:motion_ease_tune/features/home/sound_player.dart'; 12 | 13 | class HomePage extends StatefulWidget { 14 | const HomePage({super.key}); 15 | 16 | @override 17 | State createState() => _HomePageState(); 18 | } 19 | 20 | class _HomePageState extends State with SingleTickerProviderStateMixin { 21 | late SoundPlayer _soundPlayer; 22 | int _progress = 0; 23 | Timer? _timer; 24 | bool _isPlaying = false; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | 30 | // if is first launch, show guide dialog 31 | const isFirstLaunchKey = 'is-first-launch'; 32 | SharedPreferences.getInstance().then((prefs) { 33 | bool? isFirstLaunch = prefs.getBool(isFirstLaunchKey); 34 | if (isFirstLaunch == null || isFirstLaunch == true) { 35 | prefs.setBool(isFirstLaunchKey, false); 36 | if (mounted) openGuideDialog(context); 37 | } 38 | }); 39 | 40 | _soundPlayer = SoundPlayer(this, 41 | playingStateChanged: (value) { 42 | setState(() => _isPlaying = value); 43 | }, 44 | ); 45 | } 46 | 47 | @override 48 | void dispose() { 49 | _soundPlayer.release(); 50 | super.dispose(); 51 | } 52 | 53 | void startTimer() { 54 | _timer = Timer.periodic( 55 | const Duration(seconds: 1), 56 | (Timer timer) { 57 | if (timer.tick == 60) { 58 | timer.cancel(); 59 | _timer = null; 60 | setState(() => _progress = 0); 61 | _soundPlayer.stop(); 62 | return; 63 | } 64 | setState(() => _progress = timer.tick); 65 | } 66 | ); 67 | } 68 | void stopTimer() { 69 | if (_timer == null) return; 70 | setState(() => _progress = 0); 71 | _timer!.cancel(); 72 | _timer = null; 73 | } 74 | 75 | @override 76 | Widget build(BuildContext context) { 77 | final actions = [ 78 | IconButton( 79 | icon: const Icon(Icons.help), 80 | onPressed: () => openGuideDialog(context), 81 | ), 82 | IconButton( 83 | icon: const Icon(Icons.settings), 84 | onPressed: () { 85 | Navigator.of(context).push( 86 | MaterialPageRoute(builder: 87 | (context) => const SettingsPage()), 88 | ); 89 | }, 90 | ), 91 | ]; 92 | final textStyle = TextStyle( 93 | fontSize: 96, 94 | fontFamily: 'ComicRelief', 95 | color: Theme.of(context).colorScheme.primary, 96 | ); 97 | final text100Hz = Row( 98 | mainAxisAlignment: MainAxisAlignment.center, 99 | spacing: 4, 100 | children: [ 101 | Text('100', style: textStyle), 102 | Text('Hz', style: textStyle), 103 | ] 104 | ); 105 | final progress = Padding( 106 | padding: const EdgeInsets.symmetric(horizontal: 32), 107 | child: ProgressBar( 108 | total: const Duration(seconds: 60), 109 | progress: Duration(seconds: _progress), 110 | thumbColor: Theme.of(context).colorScheme.secondary, 111 | ), 112 | ); 113 | final toggleButton = ElevatedButton( 114 | style: ElevatedButton.styleFrom( 115 | backgroundColor: Theme.of(context).colorScheme.primary, 116 | padding: EdgeInsets.all(12), 117 | elevation: 4, 118 | ), 119 | onPressed: () async { 120 | if (_isPlaying) { 121 | _soundPlayer.stop(); 122 | stopTimer(); 123 | } else { 124 | final currentVolume = (await RealVolume.getCurrentVol(StreamType.MUSIC)) ?? 0.0; 125 | if (currentVolume <= 0.1) { 126 | if (context.mounted) { 127 | ScaffoldMessenger.of(context).showSnackBar( 128 | SnackBar( 129 | content: Text(AppLocalizations.of(context)!.pleaseTurnUpVolume), 130 | ), 131 | ); 132 | } 133 | return; 134 | } 135 | _soundPlayer.play(); 136 | startTimer(); 137 | } 138 | }, 139 | child: Icon( 140 | _isPlaying ? Icons.stop : Icons.play_arrow, 141 | size: 48, 142 | color: Theme.of(context).colorScheme.onPrimary, 143 | ), 144 | ); 145 | return Scaffold( 146 | appBar: AppBar( 147 | title: const Text('MotionEaseTune'), 148 | foregroundColor: Theme.of(context).colorScheme.onPrimary, 149 | backgroundColor: Theme.of(context).colorScheme.primary, 150 | actions: actions, 151 | ), 152 | body: Column( 153 | children: [ 154 | SineWave(_isPlaying), 155 | Expanded( 156 | child: Center( 157 | child: text100Hz, 158 | ) 159 | ), 160 | progress, 161 | toggleButton, 162 | const SizedBox(height: 48), 163 | ], 164 | ), 165 | ); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /lib/features/home/sine_wave.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_sinusoidals_v2/flutter_sinusoidals_v2.dart'; 5 | 6 | class SineWave extends StatefulWidget { 7 | const SineWave(this.isPlaying, {super.key}); 8 | 9 | final bool isPlaying; 10 | 11 | @override 12 | State createState() => _SineWaveState(); 13 | } 14 | 15 | class _SineWaveState extends State with SingleTickerProviderStateMixin { 16 | late AnimationController _transitionController; 17 | final _targetAmplitude = 30.0; 18 | final _targetContainerHeight = 120.0; 19 | double _amplitude = 0.0; 20 | double _containerHeight = 40.0; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | _transitionController = AnimationController( 26 | duration: const Duration(milliseconds: 300), 27 | vsync: this, 28 | ); 29 | _transitionController.addListener(() { 30 | setState(() { 31 | _amplitude = _targetAmplitude * _transitionController.value; 32 | _containerHeight = max( 33 | 40.0, 34 | _targetContainerHeight * _transitionController.value 35 | ); 36 | }); 37 | }); 38 | } 39 | 40 | @override 41 | void dispose() { 42 | _transitionController.dispose(); 43 | super.dispose(); 44 | } 45 | 46 | @override 47 | void didUpdateWidget(covariant SineWave oldWidget) { 48 | super.didUpdateWidget(oldWidget); 49 | 50 | if (widget.isPlaying == oldWidget.isPlaying) return; 51 | if (widget.isPlaying) { 52 | _transitionController.forward(from: 0.0); 53 | } else { 54 | _transitionController.reverse(from: 1.0); 55 | } 56 | } 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | return Sinusoidal( 61 | reverse: true, 62 | model: SinusoidalModel( 63 | formular: WaveFormular.normal, 64 | amplitude: _amplitude, 65 | waves: 4, 66 | frequency: 1, 67 | ), 68 | child: Container( 69 | height: _containerHeight, 70 | color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5), 71 | ), 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/features/home/sound_player.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:sound_generator/sound_generator.dart'; 3 | import 'package:sound_generator/waveTypes.dart'; 4 | 5 | class SoundPlayer { 6 | static const waveType = waveTypes.SINUSOIDAL; 7 | static const frequency = 100.0; // Frequency in Hz 8 | static const balance = 0.0; 9 | static const sampleRate = 192000; 10 | 11 | late AnimationController _volumeController; 12 | late double _targetVolume; 13 | set targetVolume(double value) => _targetVolume = value; 14 | 15 | SoundPlayer(TickerProvider tickerProvider, { 16 | double targetVolume = 1.0, 17 | Function(bool)? playingStateChanged, 18 | }) { 19 | _targetVolume = targetVolume; 20 | 21 | _volumeController = AnimationController( 22 | duration: const Duration(milliseconds: 100), 23 | vsync: tickerProvider, 24 | ); 25 | _volumeController.addListener(() { 26 | SoundGenerator.setVolume(_volumeController.value * _targetVolume); 27 | }); 28 | 29 | SoundGenerator.init(sampleRate); 30 | SoundGenerator.onIsPlayingChanged.listen(playingStateChanged); 31 | 32 | SoundGenerator.setWaveType(waveType); 33 | SoundGenerator.setBalance(balance); 34 | SoundGenerator.setVolume(1.0); 35 | SoundGenerator.setFrequency(frequency); 36 | SoundGenerator.setAutoUpdateOneCycleSample(true); 37 | SoundGenerator.refreshOneCycleData(); 38 | } 39 | 40 | play() { 41 | SoundGenerator.play(); 42 | _volumeController.forward(from: _volumeController.value); 43 | } 44 | 45 | stop() { 46 | _volumeController.reverse(from: _targetVolume).then((_) { 47 | SoundGenerator.stop(); 48 | }); 49 | } 50 | 51 | release() { 52 | SoundGenerator.release(); 53 | _volumeController.dispose(); 54 | } 55 | } -------------------------------------------------------------------------------- /lib/features/settings/about_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/gestures.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:package_info_plus/package_info_plus.dart'; 4 | import 'package:url_launcher/url_launcher.dart'; 5 | import 'package:motion_ease_tune/l10n/app_localizations.dart'; 6 | 7 | class AboutPage extends StatefulWidget { 8 | const AboutPage({super.key}); 9 | 10 | @override 11 | State createState() => _AboutPageState(); 12 | } 13 | 14 | class _AboutPageState extends State { 15 | late final _paperRec = TapGestureRecognizer()..onTap = () => 16 | _launchRelatedPaper('https://doi.org/10.1265/ehpm.24-00247'); 17 | late final _homePageRec = TapGestureRecognizer()..onTap = () => 18 | _launchRelatedPaper('https://github.com/BHznJNs/MotionEaseTune'); 19 | String _versionName = ''; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | PackageInfo.fromPlatform().then((value) { 25 | setState(() { 26 | _versionName = value.version; 27 | }); 28 | }); 29 | } 30 | 31 | @override 32 | void dispose() { 33 | _paperRec.dispose(); 34 | super.dispose(); 35 | } 36 | 37 | void _launchRelatedPaper(String url) async { 38 | final l10n = AppLocalizations.of(context)!; 39 | final uri = Uri.parse(url); 40 | if (!await launchUrl(uri)) { 41 | if (!mounted) return; 42 | ScaffoldMessenger.of(context).showSnackBar( 43 | SnackBar(content: Text(l10n.aboutPageOpenUrlError)), 44 | ); 45 | } 46 | } 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | final l10n = AppLocalizations.of(context)!; 51 | final isDarkMode = Theme.of(context).brightness == Brightness.dark; 52 | final logoAndName = Center(child: Column( 53 | children: [ 54 | const SizedBox(height: 16), 55 | ClipRRect( 56 | borderRadius: BorderRadius.circular(8.0), 57 | child: Image.asset('assets/icon/icon.png', width: 200, height: 200), 58 | ), 59 | const SizedBox(height: 16), 60 | Text('MotionEaseTune', style: TextStyle( 61 | fontSize: 36, 62 | fontFamily: 'ComicRelief', 63 | color: Theme.of(context).colorScheme.primary, 64 | )), 65 | const SizedBox(height: 32), 66 | ], 67 | )); 68 | final homePageText = Text.rich( 69 | TextSpan(children: [ 70 | WidgetSpan(child: Image.asset( 71 | isDarkMode ? 'assets/icon/github-mark-white.png' : 'assets/icon/github-mark.png', 72 | height: 20, 73 | )), 74 | TextSpan(text: ' ', style: TextStyle(fontSize: 18)), 75 | TextSpan( 76 | text: l10n.aboutPageHomepage, 77 | style: TextStyle( 78 | fontSize: 18, 79 | color: Theme.of(context).colorScheme.primary, 80 | decoration: TextDecoration.underline, 81 | ), 82 | recognizer: _homePageRec 83 | ), 84 | ]), 85 | ); 86 | final feedbackText = Text.rich( 87 | TextSpan(children: [ 88 | WidgetSpan(child: Icon(Icons.feedback, size: 20)), 89 | TextSpan(text: ' ', style: TextStyle(fontSize: 18)), 90 | TextSpan( 91 | text: l10n.aboutPageFeedback, 92 | style: TextStyle( 93 | fontSize: 18, 94 | color: Theme.of(context).colorScheme.primary, 95 | decoration: TextDecoration.underline, 96 | ), 97 | recognizer: _homePageRec 98 | ), 99 | ]), 100 | ); 101 | final thanksText = Text.rich( 102 | TextSpan(children: [ 103 | TextSpan(text: l10n.aboutPageThanksPrefix), 104 | TextSpan( 105 | text: l10n.aboutPageThanksResearch, 106 | style: TextStyle( 107 | fontSize: 18, 108 | color: Theme.of(context).colorScheme.primary, 109 | decoration: TextDecoration.underline, 110 | ), 111 | recognizer: _paperRec, 112 | ), 113 | TextSpan(text: l10n.aboutPageThanksSuffix), 114 | ]), 115 | style: TextStyle(fontSize: 18), 116 | ); 117 | return Scaffold( 118 | appBar: AppBar( 119 | title: Text(l10n.aboutPageTitle), 120 | ), 121 | body: Padding( 122 | padding: EdgeInsets.symmetric(horizontal: 32), 123 | child: Column( 124 | crossAxisAlignment: CrossAxisAlignment.start, 125 | children: [ 126 | logoAndName, 127 | homePageText, 128 | const Divider(), 129 | feedbackText, 130 | const Divider(), 131 | Row(spacing: 5, children: [ 132 | Icon(Icons.history, size: 24), 133 | Text(l10n.aboutPageVersion(_versionName), style: TextStyle(fontSize: 18)), 134 | ]), 135 | const Divider(), 136 | thanksText, 137 | ], 138 | ), 139 | ), 140 | ); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /lib/features/settings/language_settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:motion_ease_tune/l10n/app_localizations.dart'; 3 | import 'package:motion_ease_tune/providers/language_provider.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class LanguageSettings extends StatelessWidget { 7 | const LanguageSettings({super.key}); 8 | 9 | void onLanguageSelected(BuildContext context, LanguageOption? lang) { 10 | if (lang == null) return; 11 | final languageProvider = Provider.of(context, listen: false); 12 | languageProvider.setLocale(lang); 13 | } 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final l10n = AppLocalizations.of(context)!; 18 | final languageProvider = Provider.of(context); 19 | final languageListTiles = AppLocalizations.supportedLocales 20 | .map((locale) => LanguageOption.fromLocale(locale)) 21 | .map((lang) { 22 | return RadioListTile.adaptive( 23 | title: Text(lang.toLanguageDisplayName()), 24 | value: lang, 25 | groupValue: languageProvider.language, 26 | onChanged: (lang) => onLanguageSelected(context, lang), 27 | ); 28 | }).toList(); 29 | 30 | return Scaffold( 31 | appBar: AppBar(title: Text(l10n.languagesAppBarTitle)), 32 | body: ListView( 33 | children: [ 34 | RadioListTile.adaptive( 35 | title: Text(l10n.systemLanguageOption), 36 | value: LanguageOption.system, 37 | groupValue: languageProvider.language, 38 | onChanged: (lang) => onLanguageSelected(context, lang), 39 | ), 40 | ...languageListTiles, 41 | ], 42 | ), 43 | ); 44 | } 45 | } -------------------------------------------------------------------------------- /lib/features/settings/settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:motion_ease_tune/features/settings/about_page.dart'; 3 | import 'package:motion_ease_tune/features/settings/language_settings.dart'; 4 | import 'package:motion_ease_tune/l10n/app_localizations.dart'; 5 | import 'package:motion_ease_tune/providers/theme_provider.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class SettingsPage extends StatefulWidget { 9 | const SettingsPage({super.key}); 10 | 11 | @override 12 | State createState() => _SettingsPageState(); 13 | } 14 | class _SettingsPageState extends State { 15 | @override 16 | Widget build(BuildContext context) { 17 | final l10n = AppLocalizations.of(context)!; 18 | final themeProvider = Provider.of(context); 19 | final languageSettingsItem = _SettingItem( 20 | title: l10n.languagesSettingTitle, 21 | icon: Icons.language, 22 | onTap: () { 23 | Navigator.of(context).push(MaterialPageRoute( 24 | builder: (context) => const LanguageSettings(), 25 | )); 26 | }, 27 | ); 28 | final themeDropdownItems = [ 29 | DropdownMenuItem( 30 | value: ThemeMode.light, 31 | child: Text(l10n.lightThemeOption), 32 | ), 33 | DropdownMenuItem( 34 | value: ThemeMode.dark, 35 | child: Text(l10n.darkThemeOption), 36 | ), 37 | DropdownMenuItem( 38 | value: ThemeMode.system, 39 | child: Text(l10n.systemThemeOption), 40 | ), 41 | ]; 42 | final themeSettingsItem = _SettingItem( 43 | title: l10n.applicationThemeSettingTitle, 44 | icon: Icons.brightness_4, 45 | editor: DropdownButton( 46 | value: themeProvider.themeMode, 47 | items: themeDropdownItems, 48 | onChanged: (ThemeMode? newTheme) { 49 | if (newTheme == null) return; 50 | themeProvider.setThemeMode(newTheme); 51 | }, 52 | ), 53 | onTap: null, 54 | ); 55 | return Scaffold( 56 | appBar: AppBar( 57 | title: Text(l10n.settingsAppBarTitle) 58 | ), 59 | body: ListView( 60 | children: [ 61 | themeSettingsItem, 62 | languageSettingsItem, 63 | _SettingItem( 64 | title: l10n.aboutTitle, 65 | icon: Icons.info, 66 | onTap: () { 67 | Navigator.of(context).push(MaterialPageRoute( 68 | builder: (context) => const AboutPage(), 69 | )); 70 | }, 71 | ), 72 | ], 73 | ), 74 | ); 75 | } 76 | } 77 | 78 | class _SettingItem extends StatelessWidget { 79 | const _SettingItem({ 80 | required this.title, 81 | required this.icon, 82 | required this.onTap, 83 | this.editor, 84 | this.description, 85 | }); 86 | 87 | final String title; 88 | final String? description; 89 | final IconData icon; 90 | final VoidCallback? onTap; 91 | final Widget? editor; 92 | 93 | @override 94 | Widget build(BuildContext context) { 95 | return InkWell( 96 | onTap: onTap, 97 | child: ListTile( 98 | title: Text(title, style: TextStyle(fontSize: 18)), 99 | subtitle: description != null 100 | ? Text(description!) 101 | : null, 102 | leading: Icon(icon), 103 | trailing: editor, 104 | contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 105 | ), 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/l10n/app_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "en", 3 | "userGuideTitle": "User Guide", 4 | "@userGuideTitle": { 5 | "description": "The title of the user guide dialog." 6 | }, 7 | "nextButton": "Next", 8 | "@nextButton": { 9 | "description": "Label for the next button in the user guide." 10 | }, 11 | "doneButton": "Done", 12 | "@doneButton": { 13 | "description": "Label for the done button in the user guide." 14 | }, 15 | "useBeforeOnsetTitle": "Use before onset", 16 | "@useBeforeOnsetTitle": { 17 | "description": "Title for the first page of the user guide." 18 | }, 19 | "useBeforeOnsetDescription": "Use this app before the onset of motion sickness symptoms.", 20 | "@useBeforeOnsetDescription": { 21 | "description": "Description for the first page of the user guide." 22 | }, 23 | "sameVolumeToEarsTitle": "Same volume to ears", 24 | "@sameVolumeToEarsTitle": { 25 | "description": "Title for the second page of the user guide." 26 | }, 27 | "sameVolumeToEarsDescription": "Make sure the volume is the same for both ears.", 28 | "@sameVolumeToEarsDescription": { 29 | "description": "Description for the second page of the user guide." 30 | }, 31 | "listenForAMinuteTitle": "Listen for a minute", 32 | "@listenForAMinuteTitle": { 33 | "description": "Title for the third page of the user guide." 34 | }, 35 | "listenForAMinuteDescription": "Keep listening for about a minute for better results.", 36 | "@listenForAMinuteDescription": { 37 | "description": "Description for the third page of the user guide." 38 | }, 39 | "pleaseTurnUpVolume": "Please turn up the volume first.", 40 | "@pleaseTurnUpVolume": { 41 | "description": "The message to display when the volume is too low." 42 | }, 43 | "settingsAppBarTitle": "Settings", 44 | "applicationThemeSettingTitle": "Application theme", 45 | "lightThemeOption": "Light", 46 | "darkThemeOption": "Dark", 47 | "systemThemeOption": "System", 48 | "languagesSettingTitle": "Languages", 49 | "languagesAppBarTitle": "Languages", 50 | "systemLanguageOption": "System", 51 | "aboutTitle": "About", 52 | "aboutPageTitle": "About", 53 | "aboutPageHomepage": "Homepage", 54 | "@aboutPageHomepage": {"description": "Label for the homepage link in the about page."}, 55 | "aboutPageFeedback": "Feedback", 56 | "@aboutPageFeedback": {"description": "Label for the feedback link in the about page."}, 57 | "aboutPageVersion": "Version: {version}", 58 | "@aboutPageVersion": {"description": "Version label in the about page. {version} is the version string."}, 59 | "aboutPageThanksPrefix": "The core concept of this application is inspired by ", 60 | "@aboutPageThanksPrefix": {"description": "Prefix for the thanks text in the about page."}, 61 | "aboutPageThanksResearch": "this research", 62 | "@aboutPageThanksResearch": {"description": "Clickable text for the research link in the about page."}, 63 | "aboutPageThanksSuffix": ".", 64 | "@aboutPageThanksSuffix": {"description": "Suffix for the thanks text in the about page."}, 65 | "aboutPageOpenUrlError": "Could not open the link", 66 | "@aboutPageOpenUrlError": {"description": "Error message when failing to open a link in the about page."} 67 | } -------------------------------------------------------------------------------- /lib/l10n/app_localizations.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:flutter_localizations/flutter_localizations.dart'; 6 | import 'package:intl/intl.dart' as intl; 7 | 8 | import 'app_localizations_en.dart'; 9 | import 'app_localizations_zh.dart'; 10 | 11 | // ignore_for_file: type=lint 12 | 13 | /// Callers can lookup localized strings with an instance of AppLocalizations 14 | /// returned by `AppLocalizations.of(context)`. 15 | /// 16 | /// Applications need to include `AppLocalizations.delegate()` in their app's 17 | /// `localizationDelegates` list, and the locales they support in the app's 18 | /// `supportedLocales` list. For example: 19 | /// 20 | /// ```dart 21 | /// import 'l10n/app_localizations.dart'; 22 | /// 23 | /// return MaterialApp( 24 | /// localizationsDelegates: AppLocalizations.localizationsDelegates, 25 | /// supportedLocales: AppLocalizations.supportedLocales, 26 | /// home: MyApplicationHome(), 27 | /// ); 28 | /// ``` 29 | /// 30 | /// ## Update pubspec.yaml 31 | /// 32 | /// Please make sure to update your pubspec.yaml to include the following 33 | /// packages: 34 | /// 35 | /// ```yaml 36 | /// dependencies: 37 | /// # Internationalization support. 38 | /// flutter_localizations: 39 | /// sdk: flutter 40 | /// intl: any # Use the pinned version from flutter_localizations 41 | /// 42 | /// # Rest of dependencies 43 | /// ``` 44 | /// 45 | /// ## iOS Applications 46 | /// 47 | /// iOS applications define key application metadata, including supported 48 | /// locales, in an Info.plist file that is built into the application bundle. 49 | /// To configure the locales supported by your app, you’ll need to edit this 50 | /// file. 51 | /// 52 | /// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. 53 | /// Then, in the Project Navigator, open the Info.plist file under the Runner 54 | /// project’s Runner folder. 55 | /// 56 | /// Next, select the Information Property List item, select Add Item from the 57 | /// Editor menu, then select Localizations from the pop-up menu. 58 | /// 59 | /// Select and expand the newly-created Localizations item then, for each 60 | /// locale your application supports, add a new item and select the locale 61 | /// you wish to add from the pop-up menu in the Value field. This list should 62 | /// be consistent with the languages listed in the AppLocalizations.supportedLocales 63 | /// property. 64 | abstract class AppLocalizations { 65 | AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); 66 | 67 | final String localeName; 68 | 69 | static AppLocalizations? of(BuildContext context) { 70 | return Localizations.of(context, AppLocalizations); 71 | } 72 | 73 | static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); 74 | 75 | /// A list of this localizations delegate along with the default localizations 76 | /// delegates. 77 | /// 78 | /// Returns a list of localizations delegates containing this delegate along with 79 | /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, 80 | /// and GlobalWidgetsLocalizations.delegate. 81 | /// 82 | /// Additional delegates can be added by appending to this list in 83 | /// MaterialApp. This list does not have to be used at all if a custom list 84 | /// of delegates is preferred or required. 85 | static const List> localizationsDelegates = >[ 86 | delegate, 87 | GlobalMaterialLocalizations.delegate, 88 | GlobalCupertinoLocalizations.delegate, 89 | GlobalWidgetsLocalizations.delegate, 90 | ]; 91 | 92 | /// A list of this localizations delegate's supported locales. 93 | static const List supportedLocales = [ 94 | Locale('en'), 95 | Locale('zh') 96 | ]; 97 | 98 | /// The title of the user guide dialog. 99 | /// 100 | /// In en, this message translates to: 101 | /// **'User Guide'** 102 | String get userGuideTitle; 103 | 104 | /// Label for the next button in the user guide. 105 | /// 106 | /// In en, this message translates to: 107 | /// **'Next'** 108 | String get nextButton; 109 | 110 | /// Label for the done button in the user guide. 111 | /// 112 | /// In en, this message translates to: 113 | /// **'Done'** 114 | String get doneButton; 115 | 116 | /// Title for the first page of the user guide. 117 | /// 118 | /// In en, this message translates to: 119 | /// **'Use before onset'** 120 | String get useBeforeOnsetTitle; 121 | 122 | /// Description for the first page of the user guide. 123 | /// 124 | /// In en, this message translates to: 125 | /// **'Use this app before the onset of motion sickness symptoms.'** 126 | String get useBeforeOnsetDescription; 127 | 128 | /// Title for the second page of the user guide. 129 | /// 130 | /// In en, this message translates to: 131 | /// **'Same volume to ears'** 132 | String get sameVolumeToEarsTitle; 133 | 134 | /// Description for the second page of the user guide. 135 | /// 136 | /// In en, this message translates to: 137 | /// **'Make sure the volume is the same for both ears.'** 138 | String get sameVolumeToEarsDescription; 139 | 140 | /// Title for the third page of the user guide. 141 | /// 142 | /// In en, this message translates to: 143 | /// **'Listen for a minute'** 144 | String get listenForAMinuteTitle; 145 | 146 | /// Description for the third page of the user guide. 147 | /// 148 | /// In en, this message translates to: 149 | /// **'Keep listening for about a minute for better results.'** 150 | String get listenForAMinuteDescription; 151 | 152 | /// The message to display when the volume is too low. 153 | /// 154 | /// In en, this message translates to: 155 | /// **'Please turn up the volume first.'** 156 | String get pleaseTurnUpVolume; 157 | 158 | /// No description provided for @settingsAppBarTitle. 159 | /// 160 | /// In en, this message translates to: 161 | /// **'Settings'** 162 | String get settingsAppBarTitle; 163 | 164 | /// No description provided for @applicationThemeSettingTitle. 165 | /// 166 | /// In en, this message translates to: 167 | /// **'Application theme'** 168 | String get applicationThemeSettingTitle; 169 | 170 | /// No description provided for @lightThemeOption. 171 | /// 172 | /// In en, this message translates to: 173 | /// **'Light'** 174 | String get lightThemeOption; 175 | 176 | /// No description provided for @darkThemeOption. 177 | /// 178 | /// In en, this message translates to: 179 | /// **'Dark'** 180 | String get darkThemeOption; 181 | 182 | /// No description provided for @systemThemeOption. 183 | /// 184 | /// In en, this message translates to: 185 | /// **'System'** 186 | String get systemThemeOption; 187 | 188 | /// No description provided for @languagesSettingTitle. 189 | /// 190 | /// In en, this message translates to: 191 | /// **'Languages'** 192 | String get languagesSettingTitle; 193 | 194 | /// No description provided for @languagesAppBarTitle. 195 | /// 196 | /// In en, this message translates to: 197 | /// **'Languages'** 198 | String get languagesAppBarTitle; 199 | 200 | /// No description provided for @systemLanguageOption. 201 | /// 202 | /// In en, this message translates to: 203 | /// **'System'** 204 | String get systemLanguageOption; 205 | 206 | /// No description provided for @aboutTitle. 207 | /// 208 | /// In en, this message translates to: 209 | /// **'About'** 210 | String get aboutTitle; 211 | 212 | /// No description provided for @aboutPageTitle. 213 | /// 214 | /// In en, this message translates to: 215 | /// **'About'** 216 | String get aboutPageTitle; 217 | 218 | /// Label for the homepage link in the about page. 219 | /// 220 | /// In en, this message translates to: 221 | /// **'Homepage'** 222 | String get aboutPageHomepage; 223 | 224 | /// Label for the feedback link in the about page. 225 | /// 226 | /// In en, this message translates to: 227 | /// **'Feedback'** 228 | String get aboutPageFeedback; 229 | 230 | /// Version label in the about page. {version} is the version string. 231 | /// 232 | /// In en, this message translates to: 233 | /// **'Version: {version}'** 234 | String aboutPageVersion(Object version); 235 | 236 | /// Prefix for the thanks text in the about page. 237 | /// 238 | /// In en, this message translates to: 239 | /// **'The core concept of this application is inspired by '** 240 | String get aboutPageThanksPrefix; 241 | 242 | /// Clickable text for the research link in the about page. 243 | /// 244 | /// In en, this message translates to: 245 | /// **'this research'** 246 | String get aboutPageThanksResearch; 247 | 248 | /// Suffix for the thanks text in the about page. 249 | /// 250 | /// In en, this message translates to: 251 | /// **'.'** 252 | String get aboutPageThanksSuffix; 253 | 254 | /// Error message when failing to open a link in the about page. 255 | /// 256 | /// In en, this message translates to: 257 | /// **'Could not open the link'** 258 | String get aboutPageOpenUrlError; 259 | } 260 | 261 | class _AppLocalizationsDelegate extends LocalizationsDelegate { 262 | const _AppLocalizationsDelegate(); 263 | 264 | @override 265 | Future load(Locale locale) { 266 | return SynchronousFuture(lookupAppLocalizations(locale)); 267 | } 268 | 269 | @override 270 | bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode); 271 | 272 | @override 273 | bool shouldReload(_AppLocalizationsDelegate old) => false; 274 | } 275 | 276 | AppLocalizations lookupAppLocalizations(Locale locale) { 277 | 278 | 279 | // Lookup logic when only language code is specified. 280 | switch (locale.languageCode) { 281 | case 'en': return AppLocalizationsEn(); 282 | case 'zh': return AppLocalizationsZh(); 283 | } 284 | 285 | throw FlutterError( 286 | 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' 287 | 'an issue with the localizations generation tool. Please file an issue ' 288 | 'on GitHub with a reproducible sample app and the gen-l10n configuration ' 289 | 'that was used.' 290 | ); 291 | } 292 | -------------------------------------------------------------------------------- /lib/l10n/app_localizations_en.dart: -------------------------------------------------------------------------------- 1 | // ignore: unused_import 2 | import 'package:intl/intl.dart' as intl; 3 | import 'app_localizations.dart'; 4 | 5 | // ignore_for_file: type=lint 6 | 7 | /// The translations for English (`en`). 8 | class AppLocalizationsEn extends AppLocalizations { 9 | AppLocalizationsEn([String locale = 'en']) : super(locale); 10 | 11 | @override 12 | String get userGuideTitle => 'User Guide'; 13 | 14 | @override 15 | String get nextButton => 'Next'; 16 | 17 | @override 18 | String get doneButton => 'Done'; 19 | 20 | @override 21 | String get useBeforeOnsetTitle => 'Use before onset'; 22 | 23 | @override 24 | String get useBeforeOnsetDescription => 'Use this app before the onset of motion sickness symptoms.'; 25 | 26 | @override 27 | String get sameVolumeToEarsTitle => 'Same volume to ears'; 28 | 29 | @override 30 | String get sameVolumeToEarsDescription => 'Make sure the volume is the same for both ears.'; 31 | 32 | @override 33 | String get listenForAMinuteTitle => 'Listen for a minute'; 34 | 35 | @override 36 | String get listenForAMinuteDescription => 'Keep listening for about a minute for better results.'; 37 | 38 | @override 39 | String get pleaseTurnUpVolume => 'Please turn up the volume first.'; 40 | 41 | @override 42 | String get settingsAppBarTitle => 'Settings'; 43 | 44 | @override 45 | String get applicationThemeSettingTitle => 'Application theme'; 46 | 47 | @override 48 | String get lightThemeOption => 'Light'; 49 | 50 | @override 51 | String get darkThemeOption => 'Dark'; 52 | 53 | @override 54 | String get systemThemeOption => 'System'; 55 | 56 | @override 57 | String get languagesSettingTitle => 'Languages'; 58 | 59 | @override 60 | String get languagesAppBarTitle => 'Languages'; 61 | 62 | @override 63 | String get systemLanguageOption => 'System'; 64 | 65 | @override 66 | String get aboutTitle => 'About'; 67 | 68 | @override 69 | String get aboutPageTitle => 'About'; 70 | 71 | @override 72 | String get aboutPageHomepage => 'Homepage'; 73 | 74 | @override 75 | String get aboutPageFeedback => 'Feedback'; 76 | 77 | @override 78 | String aboutPageVersion(Object version) { 79 | return 'Version: $version'; 80 | } 81 | 82 | @override 83 | String get aboutPageThanksPrefix => 'The core concept of this application is inspired by '; 84 | 85 | @override 86 | String get aboutPageThanksResearch => 'this research'; 87 | 88 | @override 89 | String get aboutPageThanksSuffix => '.'; 90 | 91 | @override 92 | String get aboutPageOpenUrlError => 'Could not open the link'; 93 | } 94 | -------------------------------------------------------------------------------- /lib/l10n/app_localizations_zh.dart: -------------------------------------------------------------------------------- 1 | // ignore: unused_import 2 | import 'package:intl/intl.dart' as intl; 3 | import 'app_localizations.dart'; 4 | 5 | // ignore_for_file: type=lint 6 | 7 | /// The translations for Chinese (`zh`). 8 | class AppLocalizationsZh extends AppLocalizations { 9 | AppLocalizationsZh([String locale = 'zh']) : super(locale); 10 | 11 | @override 12 | String get userGuideTitle => '用户指南'; 13 | 14 | @override 15 | String get nextButton => '下一步'; 16 | 17 | @override 18 | String get doneButton => '完成'; 19 | 20 | @override 21 | String get useBeforeOnsetTitle => '在症状出现前使用'; 22 | 23 | @override 24 | String get useBeforeOnsetDescription => '为了获得最佳效果,建议您在感觉到晕车症状之前使用本应用。'; 25 | 26 | @override 27 | String get sameVolumeToEarsTitle => '留意双耳音量'; 28 | 29 | @override 30 | String get sameVolumeToEarsDescription => '为了获得最佳效果,请尽量让双耳听到的音量感觉一致。'; 31 | 32 | @override 33 | String get listenForAMinuteTitle => '聆听一分钟左右'; 34 | 35 | @override 36 | String get listenForAMinuteDescription => '持续聆听音频一分钟,有助于达到更佳效果。'; 37 | 38 | @override 39 | String get pleaseTurnUpVolume => '请先调高音量。'; 40 | 41 | @override 42 | String get settingsAppBarTitle => '设置'; 43 | 44 | @override 45 | String get applicationThemeSettingTitle => '应用主题'; 46 | 47 | @override 48 | String get lightThemeOption => '浅色'; 49 | 50 | @override 51 | String get darkThemeOption => '深色'; 52 | 53 | @override 54 | String get systemThemeOption => '跟随系统'; 55 | 56 | @override 57 | String get languagesSettingTitle => '语言'; 58 | 59 | @override 60 | String get languagesAppBarTitle => '语言'; 61 | 62 | @override 63 | String get systemLanguageOption => '跟随系统'; 64 | 65 | @override 66 | String get aboutTitle => '关于'; 67 | 68 | @override 69 | String get aboutPageTitle => '关于应用'; 70 | 71 | @override 72 | String get aboutPageHomepage => '主页'; 73 | 74 | @override 75 | String get aboutPageFeedback => '反馈'; 76 | 77 | @override 78 | String aboutPageVersion(Object version) { 79 | return '版本:$version'; 80 | } 81 | 82 | @override 83 | String get aboutPageThanksPrefix => '本应用的核心理念来源于'; 84 | 85 | @override 86 | String get aboutPageThanksResearch => '这项研究'; 87 | 88 | @override 89 | String get aboutPageThanksSuffix => '。'; 90 | 91 | @override 92 | String get aboutPageOpenUrlError => '无法打开链接'; 93 | } 94 | -------------------------------------------------------------------------------- /lib/l10n/app_zh.arb: -------------------------------------------------------------------------------- 1 | { 2 | "@@locale": "zh", 3 | "userGuideTitle": "用户指南", 4 | "@userGuideTitle": { 5 | "description": "用户指南对话框的标题。" 6 | }, 7 | "nextButton": "下一步", 8 | "@nextButton": { 9 | "description": "用户指南中下一步按钮的标签。" 10 | }, 11 | "doneButton": "完成", 12 | "@doneButton": { 13 | "description": "用户指南中完成按钮的标签。" 14 | }, 15 | "useBeforeOnsetTitle": "在症状出现前使用", 16 | "@useBeforeOnsetTitle": { 17 | "description": "用户指南第一页的标题。" 18 | }, 19 | "useBeforeOnsetDescription": "为了获得最佳效果,建议您在感觉到晕车症状之前使用本应用。", 20 | "@useBeforeOnsetDescription": { 21 | "description": "用户指南第一页的描述。" 22 | }, 23 | "sameVolumeToEarsTitle": "留意双耳音量", 24 | "@sameVolumeToEarsTitle": { 25 | "description": "用户指南第二页的标题。" 26 | }, 27 | "sameVolumeToEarsDescription": "为了获得最佳效果,请尽量让双耳听到的音量感觉一致。", 28 | "@sameVolumeToEarsDescription": { 29 | "description": "用户指南第二页的描述。" 30 | }, 31 | "listenForAMinuteTitle": "聆听一分钟左右", 32 | "@listenForAMinuteTitle": { 33 | "description": "用户指南第三页的标题。" 34 | }, 35 | "listenForAMinuteDescription": "持续聆听音频一分钟,有助于达到更佳效果。", 36 | "@listenForAMinuteDescription": { 37 | "description": "用户指南第三页的描述。" 38 | }, 39 | "pleaseTurnUpVolume": "请先调高音量。", 40 | "@pleaseTurnUpVolume": { 41 | "description": "音量过低时显示的提示信息。" 42 | }, 43 | "settingsAppBarTitle": "设置", 44 | "applicationThemeSettingTitle": "应用主题", 45 | "lightThemeOption": "浅色", 46 | "darkThemeOption": "深色", 47 | "systemThemeOption": "跟随系统", 48 | "languagesSettingTitle": "语言", 49 | "languagesAppBarTitle": "语言", 50 | "systemLanguageOption": "跟随系统", 51 | "aboutTitle": "关于", 52 | "@aboutTitle": { 53 | "description": "“关于”菜单项或按钮的标签。" 54 | }, 55 | "aboutPageTitle": "关于应用", 56 | "@aboutPageTitle": { 57 | "description": "“关于”页面的标题。" 58 | }, 59 | "aboutPageHomepage": "主页", 60 | "@aboutPageHomepage": {"description": "关于页面主页链接的标签。"}, 61 | "aboutPageFeedback": "反馈", 62 | "@aboutPageFeedback": {"description": "关于页面反馈链接的标签。"}, 63 | "aboutPageVersion": "版本:{version}", 64 | "@aboutPageVersion": {"description": "关于页面的版本标签。{version} 是版本字符串。"}, 65 | "aboutPageThanksPrefix": "本应用的核心理念来源于", 66 | "@aboutPageThanksPrefix": {"description": "关于页面致谢文本的前缀。"}, 67 | "aboutPageThanksResearch": "这项研究", 68 | "@aboutPageThanksResearch": {"description": "关于页面研究链接的可点击文本。"}, 69 | "aboutPageThanksSuffix": "。", 70 | "@aboutPageThanksSuffix": {"description": "关于页面致谢文本的后缀。"}, 71 | "aboutPageOpenUrlError": "无法打开链接", 72 | "@aboutPageOpenUrlError": {"description": "关于页面打开链接失败时的错误信息。"} 73 | } -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:dynamic_color/dynamic_color.dart'; 3 | import 'package:motion_ease_tune/providers/language_provider.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:motion_ease_tune/l10n/app_localizations.dart'; 6 | import 'package:motion_ease_tune/features/home/home.dart'; 7 | import 'package:motion_ease_tune/providers/theme_provider.dart'; 8 | import 'package:motion_ease_tune/theme.dart'; 9 | 10 | class AppEntry extends StatefulWidget { 11 | const AppEntry({super.key}); 12 | 13 | @override 14 | State createState() => _AppEntryState(); 15 | } 16 | class _AppEntryState extends State { 17 | @override 18 | Widget build(BuildContext context) { 19 | final themeProvider = Provider.of(context); 20 | final languageProvider = Provider.of(context); 21 | return DynamicColorBuilder( 22 | builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { 23 | return MaterialApp( 24 | title: 'MotionEaseTune', 25 | theme: ThemeData( 26 | colorScheme: MaterialTheme.lightScheme(), 27 | ), 28 | darkTheme: ThemeData( 29 | colorScheme: MaterialTheme.darkScheme(), 30 | ), 31 | themeMode: themeProvider.themeMode, 32 | locale: languageProvider.language.toLocale(), 33 | localizationsDelegates: AppLocalizations.localizationsDelegates, 34 | supportedLocales: AppLocalizations.supportedLocales, 35 | home: const HomePage(), 36 | ); 37 | }, 38 | ); 39 | } 40 | } 41 | 42 | void main() { 43 | WidgetsFlutterBinding.ensureInitialized(); 44 | final entry = MultiProvider( 45 | providers: [ 46 | ChangeNotifierProvider(create: (_) => ThemeProvider()), 47 | ChangeNotifierProvider(create: (_) => LanguageProvider()), 48 | ], 49 | child: const AppEntry(), 50 | ); 51 | runApp(entry); 52 | } 53 | -------------------------------------------------------------------------------- /lib/providers/language_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | enum LanguageOption { 5 | system, 6 | english, 7 | simplifiedChinese; 8 | 9 | static LanguageOption fromLocale(Locale locale) { 10 | if (locale.languageCode == 'en') { 11 | return LanguageOption.english; 12 | } else 13 | if (locale.languageCode == 'zh') { 14 | return LanguageOption.simplifiedChinese; 15 | } 16 | throw Exception('Unresolved locale: $locale'); 17 | } 18 | 19 | Locale? toLocale() { 20 | switch (this) { 21 | case LanguageOption.simplifiedChinese: return Locale('zh', 'CN'); 22 | case LanguageOption.english: return Locale('en'); 23 | case LanguageOption.system: return null; 24 | } 25 | } 26 | 27 | String toLanguageDisplayName() { 28 | switch (this) { 29 | case LanguageOption.simplifiedChinese: return '简体中文'; 30 | case LanguageOption.english: return 'English'; 31 | default: throw Exception('Unresolved locale: $this'); 32 | } 33 | } 34 | } 35 | 36 | class LanguageProvider with ChangeNotifier { 37 | static const _langKey = 'language-index'; 38 | LanguageOption _lang = LanguageOption.system; 39 | LanguageOption get language => _lang; 40 | 41 | LanguageProvider() { 42 | _loadLocalePreference(); 43 | } 44 | 45 | void setLocale(LanguageOption newLang) async { 46 | if (_lang != newLang) { 47 | _lang = newLang; 48 | notifyListeners(); 49 | _saveLocalePreference(newLang); 50 | } 51 | } 52 | 53 | void _loadLocalePreference() async { 54 | final prefs = await SharedPreferences.getInstance(); 55 | final int? langIndex = prefs.getInt(_langKey); 56 | if (langIndex != null && langIndex >= 0 && langIndex < LanguageOption.values.length) { 57 | _lang = LanguageOption.values[langIndex]; 58 | notifyListeners(); 59 | } else { 60 | _lang = LanguageOption.system; 61 | _saveLocalePreference(_lang); 62 | notifyListeners(); 63 | } 64 | } 65 | 66 | void _saveLocalePreference(LanguageOption lang) async { 67 | final prefs = await SharedPreferences.getInstance(); 68 | prefs.setInt(_langKey, lang.index); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/providers/theme_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | class ThemeProvider with ChangeNotifier { 5 | static const _themeModeKey = 'theme-mode-index'; 6 | ThemeMode _themeMode = ThemeMode.system; 7 | ThemeMode get themeMode => _themeMode; 8 | 9 | ThemeProvider() { 10 | _loadThemePreference(); 11 | } 12 | 13 | void setThemeMode(ThemeMode newMode) async { 14 | if (_themeMode != newMode) { 15 | _themeMode = newMode; 16 | notifyListeners(); 17 | _saveThemePreference(newMode); 18 | } 19 | } 20 | 21 | void _loadThemePreference() async { 22 | final prefs = await SharedPreferences.getInstance(); 23 | final int? themeIndex = prefs.getInt(_themeModeKey); 24 | if (themeIndex != null && themeIndex >= 0 && themeIndex < ThemeMode.values.length) { 25 | _themeMode = ThemeMode.values[themeIndex]; 26 | notifyListeners(); 27 | } else { 28 | _themeMode = ThemeMode.system; 29 | _saveThemePreference(_themeMode); 30 | notifyListeners(); 31 | } 32 | } 33 | void _saveThemePreference(ThemeMode option) async { 34 | final prefs = await SharedPreferences.getInstance(); 35 | prefs.setInt(_themeModeKey, option.index); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/theme.dart: -------------------------------------------------------------------------------- 1 | import "package:flutter/material.dart"; 2 | 3 | class MaterialTheme { 4 | final TextTheme textTheme; 5 | 6 | const MaterialTheme(this.textTheme); 7 | 8 | static ColorScheme lightScheme() { 9 | return const ColorScheme( 10 | brightness: Brightness.light, 11 | primary: Color(0xff865308), 12 | surfaceTint: Color(0xff865308), 13 | onPrimary: Color(0xffffffff), 14 | primaryContainer: Color(0xfff1ad5f), 15 | onPrimaryContainer: Color(0xff6c4000), 16 | secondary: Color(0xff75593a), 17 | onSecondary: Color(0xffffffff), 18 | secondaryContainer: Color(0xfffdd6af), 19 | onSecondaryContainer: Color(0xff785b3c), 20 | tertiary: Color(0xff5a6414), 21 | onTertiary: Color(0xffffffff), 22 | tertiaryContainer: Color(0xffb6c269), 23 | onTertiaryContainer: Color(0xff464f00), 24 | error: Color(0xffba1a1a), 25 | onError: Color(0xffffffff), 26 | errorContainer: Color(0xffffdad6), 27 | onErrorContainer: Color(0xff93000a), 28 | surface: Color(0xfffff8f4), 29 | onSurface: Color(0xff201b15), 30 | onSurfaceVariant: Color(0xff514438), 31 | outline: Color(0xff847466), 32 | outlineVariant: Color(0xffd6c3b3), 33 | shadow: Color(0xff000000), 34 | scrim: Color(0xff000000), 35 | inverseSurface: Color(0xff362f29), 36 | inversePrimary: Color(0xfffeb969), 37 | primaryFixed: Color(0xffffddbb), 38 | onPrimaryFixed: Color(0xff2b1700), 39 | primaryFixedDim: Color(0xfffeb969), 40 | onPrimaryFixedVariant: Color(0xff673d00), 41 | secondaryFixed: Color(0xffffddbb), 42 | onSecondaryFixed: Color(0xff2b1701), 43 | secondaryFixedDim: Color(0xffe5c09a), 44 | onSecondaryFixedVariant: Color(0xff5b4225), 45 | tertiaryFixed: Color(0xffdeea8c), 46 | onTertiaryFixed: Color(0xff191e00), 47 | tertiaryFixedDim: Color(0xffc2ce73), 48 | onTertiaryFixedVariant: Color(0xff424b00), 49 | surfaceDim: Color(0xffe4d8cf), 50 | surfaceBright: Color(0xfffff8f4), 51 | surfaceContainerLowest: Color(0xffffffff), 52 | surfaceContainerLow: Color(0xfffef1e8), 53 | surfaceContainer: Color(0xfff8ece2), 54 | surfaceContainerHigh: Color(0xfff2e6dd), 55 | surfaceContainerHighest: Color(0xffede0d7), 56 | ); 57 | } 58 | 59 | ThemeData light() { 60 | return theme(lightScheme()); 61 | } 62 | 63 | static ColorScheme lightMediumContrastScheme() { 64 | return const ColorScheme( 65 | brightness: Brightness.light, 66 | primary: Color(0xff502e00), 67 | surfaceTint: Color(0xff865308), 68 | onPrimary: Color(0xffffffff), 69 | primaryContainer: Color(0xff986119), 70 | onPrimaryContainer: Color(0xffffffff), 71 | secondary: Color(0xff493116), 72 | onSecondary: Color(0xffffffff), 73 | secondaryContainer: Color(0xff856747), 74 | onSecondaryContainer: Color(0xffffffff), 75 | tertiary: Color(0xff333a00), 76 | onTertiary: Color(0xffffffff), 77 | tertiaryContainer: Color(0xff687323), 78 | onTertiaryContainer: Color(0xffffffff), 79 | error: Color(0xff740006), 80 | onError: Color(0xffffffff), 81 | errorContainer: Color(0xffcf2c27), 82 | onErrorContainer: Color(0xffffffff), 83 | surface: Color(0xfffff8f4), 84 | onSurface: Color(0xff15100b), 85 | onSurfaceVariant: Color(0xff403428), 86 | outline: Color(0xff5d5043), 87 | outlineVariant: Color(0xff796a5d), 88 | shadow: Color(0xff000000), 89 | scrim: Color(0xff000000), 90 | inverseSurface: Color(0xff362f29), 91 | inversePrimary: Color(0xfffeb969), 92 | primaryFixed: Color(0xff986119), 93 | onPrimaryFixed: Color(0xffffffff), 94 | primaryFixedDim: Color(0xff7b4900), 95 | onPrimaryFixedVariant: Color(0xffffffff), 96 | secondaryFixed: Color(0xff856747), 97 | onSecondaryFixed: Color(0xffffffff), 98 | secondaryFixedDim: Color(0xff6b4f31), 99 | onSecondaryFixedVariant: Color(0xffffffff), 100 | tertiaryFixed: Color(0xff687323), 101 | onTertiaryFixed: Color(0xffffffff), 102 | tertiaryFixedDim: Color(0xff505a09), 103 | onTertiaryFixedVariant: Color(0xffffffff), 104 | surfaceDim: Color(0xffd0c4bc), 105 | surfaceBright: Color(0xfffff8f4), 106 | surfaceContainerLowest: Color(0xffffffff), 107 | surfaceContainerLow: Color(0xfffef1e8), 108 | surfaceContainer: Color(0xfff2e6dd), 109 | surfaceContainerHigh: Color(0xffe7dbd1), 110 | surfaceContainerHighest: Color(0xffdbd0c6), 111 | ); 112 | } 113 | 114 | ThemeData lightMediumContrast() { 115 | return theme(lightMediumContrastScheme()); 116 | } 117 | 118 | static ColorScheme lightHighContrastScheme() { 119 | return const ColorScheme( 120 | brightness: Brightness.light, 121 | primary: Color(0xff422500), 122 | surfaceTint: Color(0xff865308), 123 | onPrimary: Color(0xffffffff), 124 | primaryContainer: Color(0xff6a3f00), 125 | onPrimaryContainer: Color(0xffffffff), 126 | secondary: Color(0xff3e270d), 127 | onSecondary: Color(0xffffffff), 128 | secondaryContainer: Color(0xff5e4427), 129 | onSecondaryContainer: Color(0xffffffff), 130 | tertiary: Color(0xff292f00), 131 | onTertiary: Color(0xffffffff), 132 | tertiaryContainer: Color(0xff454e00), 133 | onTertiaryContainer: Color(0xffffffff), 134 | error: Color(0xff600004), 135 | onError: Color(0xffffffff), 136 | errorContainer: Color(0xff98000a), 137 | onErrorContainer: Color(0xffffffff), 138 | surface: Color(0xfffff8f4), 139 | onSurface: Color(0xff000000), 140 | onSurfaceVariant: Color(0xff000000), 141 | outline: Color(0xff352a1f), 142 | outlineVariant: Color(0xff54473a), 143 | shadow: Color(0xff000000), 144 | scrim: Color(0xff000000), 145 | inverseSurface: Color(0xff362f29), 146 | inversePrimary: Color(0xfffeb969), 147 | primaryFixed: Color(0xff6a3f00), 148 | onPrimaryFixed: Color(0xffffffff), 149 | primaryFixedDim: Color(0xff4b2b00), 150 | onPrimaryFixedVariant: Color(0xffffffff), 151 | secondaryFixed: Color(0xff5e4427), 152 | onSecondaryFixed: Color(0xffffffff), 153 | secondaryFixedDim: Color(0xff452e13), 154 | onSecondaryFixedVariant: Color(0xffffffff), 155 | tertiaryFixed: Color(0xff454e00), 156 | onTertiaryFixed: Color(0xffffffff), 157 | tertiaryFixedDim: Color(0xff2f3600), 158 | onTertiaryFixedVariant: Color(0xffffffff), 159 | surfaceDim: Color(0xffc2b7ae), 160 | surfaceBright: Color(0xfffff8f4), 161 | surfaceContainerLowest: Color(0xffffffff), 162 | surfaceContainerLow: Color(0xfffbeee5), 163 | surfaceContainer: Color(0xffede0d7), 164 | surfaceContainerHigh: Color(0xffded2c9), 165 | surfaceContainerHighest: Color(0xffd0c4bc), 166 | ); 167 | } 168 | 169 | ThemeData lightHighContrast() { 170 | return theme(lightHighContrastScheme()); 171 | } 172 | 173 | static ColorScheme darkScheme() { 174 | return const ColorScheme( 175 | brightness: Brightness.dark, 176 | primary: Color(0xffffcd99), 177 | surfaceTint: Color(0xfffeb969), 178 | onPrimary: Color(0xff482900), 179 | primaryContainer: Color(0xfff1ad5f), 180 | onPrimaryContainer: Color(0xff6c4000), 181 | secondary: Color(0xffe5c09a), 182 | onSecondary: Color(0xff432c11), 183 | secondaryContainer: Color(0xff5e4427), 184 | onSecondaryContainer: Color(0xffd6b28d), 185 | tertiary: Color(0xffd2de82), 186 | onTertiary: Color(0xff2d3400), 187 | tertiaryContainer: Color(0xffb6c269), 188 | onTertiaryContainer: Color(0xff464f00), 189 | error: Color(0xffffb4ab), 190 | onError: Color(0xff690005), 191 | errorContainer: Color(0xff93000a), 192 | onErrorContainer: Color(0xffffdad6), 193 | surface: Color(0xff18120d), 194 | onSurface: Color(0xffede0d7), 195 | onSurfaceVariant: Color(0xffd6c3b3), 196 | outline: Color(0xff9e8e7f), 197 | outlineVariant: Color(0xff514438), 198 | shadow: Color(0xff000000), 199 | scrim: Color(0xff000000), 200 | inverseSurface: Color(0xffede0d7), 201 | inversePrimary: Color(0xff865308), 202 | primaryFixed: Color(0xffffddbb), 203 | onPrimaryFixed: Color(0xff2b1700), 204 | primaryFixedDim: Color(0xfffeb969), 205 | onPrimaryFixedVariant: Color(0xff673d00), 206 | secondaryFixed: Color(0xffffddbb), 207 | onSecondaryFixed: Color(0xff2b1701), 208 | secondaryFixedDim: Color(0xffe5c09a), 209 | onSecondaryFixedVariant: Color(0xff5b4225), 210 | tertiaryFixed: Color(0xffdeea8c), 211 | onTertiaryFixed: Color(0xff191e00), 212 | tertiaryFixedDim: Color(0xffc2ce73), 213 | onTertiaryFixedVariant: Color(0xff424b00), 214 | surfaceDim: Color(0xff18120d), 215 | surfaceBright: Color(0xff3f3832), 216 | surfaceContainerLowest: Color(0xff120d08), 217 | surfaceContainerLow: Color(0xff201b15), 218 | surfaceContainer: Color(0xff241f19), 219 | surfaceContainerHigh: Color(0xff2f2923), 220 | surfaceContainerHighest: Color(0xff3a342d), 221 | ); 222 | } 223 | 224 | ThemeData dark() { 225 | return theme(darkScheme()); 226 | } 227 | 228 | static ColorScheme darkMediumContrastScheme() { 229 | return const ColorScheme( 230 | brightness: Brightness.dark, 231 | primary: Color(0xffffd5aa), 232 | surfaceTint: Color(0xfffeb969), 233 | onPrimary: Color(0xff392000), 234 | primaryContainer: Color(0xfff1ad5f), 235 | onPrimaryContainer: Color(0xff442700), 236 | secondary: Color(0xfffdd5ae), 237 | onSecondary: Color(0xff362107), 238 | secondaryContainer: Color(0xffac8b68), 239 | onSecondaryContainer: Color(0xff000000), 240 | tertiary: Color(0xffd7e487), 241 | onTertiary: Color(0xff232800), 242 | tertiaryContainer: Color(0xffb6c269), 243 | onTertiaryContainer: Color(0xff2b3100), 244 | error: Color(0xffffd2cc), 245 | onError: Color(0xff540003), 246 | errorContainer: Color(0xffff5449), 247 | onErrorContainer: Color(0xff000000), 248 | surface: Color(0xff18120d), 249 | onSurface: Color(0xffffffff), 250 | onSurfaceVariant: Color(0xffecd9c8), 251 | outline: Color(0xffc1af9f), 252 | outlineVariant: Color(0xff9e8d7e), 253 | shadow: Color(0xff000000), 254 | scrim: Color(0xff000000), 255 | inverseSurface: Color(0xffede0d7), 256 | inversePrimary: Color(0xff693e00), 257 | primaryFixed: Color(0xffffddbb), 258 | onPrimaryFixed: Color(0xff1d0e00), 259 | primaryFixedDim: Color(0xfffeb969), 260 | onPrimaryFixedVariant: Color(0xff502e00), 261 | secondaryFixed: Color(0xffffddbb), 262 | onSecondaryFixed: Color(0xff1d0e00), 263 | secondaryFixedDim: Color(0xffe5c09a), 264 | onSecondaryFixedVariant: Color(0xff493116), 265 | tertiaryFixed: Color(0xffdeea8c), 266 | onTertiaryFixed: Color(0xff0f1300), 267 | tertiaryFixedDim: Color(0xffc2ce73), 268 | onTertiaryFixedVariant: Color(0xff333a00), 269 | surfaceDim: Color(0xff18120d), 270 | surfaceBright: Color(0xff4b433c), 271 | surfaceContainerLowest: Color(0xff0b0703), 272 | surfaceContainerLow: Color(0xff221d17), 273 | surfaceContainer: Color(0xff2d2721), 274 | surfaceContainerHigh: Color(0xff38312b), 275 | surfaceContainerHighest: Color(0xff443c36), 276 | ); 277 | } 278 | 279 | ThemeData darkMediumContrast() { 280 | return theme(darkMediumContrastScheme()); 281 | } 282 | 283 | static ColorScheme darkHighContrastScheme() { 284 | return const ColorScheme( 285 | brightness: Brightness.dark, 286 | primary: Color(0xffffeddd), 287 | surfaceTint: Color(0xfffeb969), 288 | onPrimary: Color(0xff000000), 289 | primaryContainer: Color(0xfffab566), 290 | onPrimaryContainer: Color(0xff150800), 291 | secondary: Color(0xffffeddd), 292 | onSecondary: Color(0xff000000), 293 | secondaryContainer: Color(0xffe1bc96), 294 | onSecondaryContainer: Color(0xff150800), 295 | tertiary: Color(0xffebf898), 296 | onTertiary: Color(0xff000000), 297 | tertiaryContainer: Color(0xffbeca70), 298 | onTertiaryContainer: Color(0xff0a0d00), 299 | error: Color(0xffffece9), 300 | onError: Color(0xff000000), 301 | errorContainer: Color(0xffffaea4), 302 | onErrorContainer: Color(0xff220001), 303 | surface: Color(0xff18120d), 304 | onSurface: Color(0xffffffff), 305 | onSurfaceVariant: Color(0xffffffff), 306 | outline: Color(0xffffeddd), 307 | outlineVariant: Color(0xffd2bfaf), 308 | shadow: Color(0xff000000), 309 | scrim: Color(0xff000000), 310 | inverseSurface: Color(0xffede0d7), 311 | inversePrimary: Color(0xff693e00), 312 | primaryFixed: Color(0xffffddbb), 313 | onPrimaryFixed: Color(0xff000000), 314 | primaryFixedDim: Color(0xfffeb969), 315 | onPrimaryFixedVariant: Color(0xff1d0e00), 316 | secondaryFixed: Color(0xffffddbb), 317 | onSecondaryFixed: Color(0xff000000), 318 | secondaryFixedDim: Color(0xffe5c09a), 319 | onSecondaryFixedVariant: Color(0xff1d0e00), 320 | tertiaryFixed: Color(0xffdeea8c), 321 | onTertiaryFixed: Color(0xff000000), 322 | tertiaryFixedDim: Color(0xffc2ce73), 323 | onTertiaryFixedVariant: Color(0xff0f1300), 324 | surfaceDim: Color(0xff18120d), 325 | surfaceBright: Color(0xff564f48), 326 | surfaceContainerLowest: Color(0xff000000), 327 | surfaceContainerLow: Color(0xff241f19), 328 | surfaceContainer: Color(0xff362f29), 329 | surfaceContainerHigh: Color(0xff413a34), 330 | surfaceContainerHighest: Color(0xff4d453f), 331 | ); 332 | } 333 | 334 | ThemeData darkHighContrast() { 335 | return theme(darkHighContrastScheme()); 336 | } 337 | 338 | 339 | ThemeData theme(ColorScheme colorScheme) => ThemeData( 340 | useMaterial3: true, 341 | brightness: colorScheme.brightness, 342 | colorScheme: colorScheme, 343 | textTheme: textTheme.apply( 344 | bodyColor: colorScheme.onSurface, 345 | displayColor: colorScheme.onSurface, 346 | ), 347 | scaffoldBackgroundColor: colorScheme.background, 348 | canvasColor: colorScheme.surface, 349 | ); 350 | 351 | 352 | List get extendedColors => [ 353 | ]; 354 | } 355 | 356 | class ExtendedColor { 357 | final Color seed, value; 358 | final ColorFamily light; 359 | final ColorFamily lightHighContrast; 360 | final ColorFamily lightMediumContrast; 361 | final ColorFamily dark; 362 | final ColorFamily darkHighContrast; 363 | final ColorFamily darkMediumContrast; 364 | 365 | const ExtendedColor({ 366 | required this.seed, 367 | required this.value, 368 | required this.light, 369 | required this.lightHighContrast, 370 | required this.lightMediumContrast, 371 | required this.dark, 372 | required this.darkHighContrast, 373 | required this.darkMediumContrast, 374 | }); 375 | } 376 | 377 | class ColorFamily { 378 | const ColorFamily({ 379 | required this.color, 380 | required this.onColor, 381 | required this.colorContainer, 382 | required this.onColorContainer, 383 | }); 384 | 385 | final Color color; 386 | final Color onColor; 387 | final Color colorContainer; 388 | final Color onColorContainer; 389 | } 390 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | sha256: a7f37ff061d7abc2fcf213554b9dcaca713c5853afa5c065c44888bc9ccaf813 9 | url: "https://pub.flutter-io.cn" 10 | source: hosted 11 | version: "4.0.6" 12 | args: 13 | dependency: transitive 14 | description: 15 | name: args 16 | sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 17 | url: "https://pub.flutter-io.cn" 18 | source: hosted 19 | version: "2.7.0" 20 | async: 21 | dependency: transitive 22 | description: 23 | name: async 24 | sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 25 | url: "https://pub.flutter-io.cn" 26 | source: hosted 27 | version: "2.12.0" 28 | audio_video_progress_bar: 29 | dependency: "direct main" 30 | description: 31 | name: audio_video_progress_bar 32 | sha256: "552b1f73c56c4c88407999e0a8507176f60c56de3e6d63bc20a0eab48467d4c9" 33 | url: "https://pub.flutter-io.cn" 34 | source: hosted 35 | version: "2.0.3" 36 | boolean_selector: 37 | dependency: transitive 38 | description: 39 | name: boolean_selector 40 | sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" 41 | url: "https://pub.flutter-io.cn" 42 | source: hosted 43 | version: "2.1.2" 44 | characters: 45 | dependency: transitive 46 | description: 47 | name: characters 48 | sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 49 | url: "https://pub.flutter-io.cn" 50 | source: hosted 51 | version: "1.4.0" 52 | checked_yaml: 53 | dependency: transitive 54 | description: 55 | name: checked_yaml 56 | sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff 57 | url: "https://pub.flutter-io.cn" 58 | source: hosted 59 | version: "2.0.3" 60 | cli_util: 61 | dependency: transitive 62 | description: 63 | name: cli_util 64 | sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c 65 | url: "https://pub.flutter-io.cn" 66 | source: hosted 67 | version: "0.4.2" 68 | clock: 69 | dependency: transitive 70 | description: 71 | name: clock 72 | sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b 73 | url: "https://pub.flutter-io.cn" 74 | source: hosted 75 | version: "1.1.2" 76 | collection: 77 | dependency: transitive 78 | description: 79 | name: collection 80 | sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" 81 | url: "https://pub.flutter-io.cn" 82 | source: hosted 83 | version: "1.19.1" 84 | crypto: 85 | dependency: transitive 86 | description: 87 | name: crypto 88 | sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" 89 | url: "https://pub.flutter-io.cn" 90 | source: hosted 91 | version: "3.0.6" 92 | cupertino_icons: 93 | dependency: "direct main" 94 | description: 95 | name: cupertino_icons 96 | sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 97 | url: "https://pub.flutter-io.cn" 98 | source: hosted 99 | version: "1.0.8" 100 | dynamic_color: 101 | dependency: "direct main" 102 | description: 103 | name: dynamic_color 104 | sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d 105 | url: "https://pub.flutter-io.cn" 106 | source: hosted 107 | version: "1.7.0" 108 | equatable: 109 | dependency: transitive 110 | description: 111 | name: equatable 112 | sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" 113 | url: "https://pub.flutter-io.cn" 114 | source: hosted 115 | version: "2.0.7" 116 | fake_async: 117 | dependency: transitive 118 | description: 119 | name: fake_async 120 | sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" 121 | url: "https://pub.flutter-io.cn" 122 | source: hosted 123 | version: "1.3.2" 124 | ffi: 125 | dependency: transitive 126 | description: 127 | name: ffi 128 | sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" 129 | url: "https://pub.flutter-io.cn" 130 | source: hosted 131 | version: "2.1.4" 132 | file: 133 | dependency: transitive 134 | description: 135 | name: file 136 | sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 137 | url: "https://pub.flutter-io.cn" 138 | source: hosted 139 | version: "7.0.1" 140 | flutter: 141 | dependency: "direct main" 142 | description: flutter 143 | source: sdk 144 | version: "0.0.0" 145 | flutter_launcher_icons: 146 | dependency: "direct dev" 147 | description: 148 | name: flutter_launcher_icons 149 | sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c 150 | url: "https://pub.flutter-io.cn" 151 | source: hosted 152 | version: "0.14.3" 153 | flutter_lints: 154 | dependency: "direct dev" 155 | description: 156 | name: flutter_lints 157 | sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" 158 | url: "https://pub.flutter-io.cn" 159 | source: hosted 160 | version: "5.0.0" 161 | flutter_localizations: 162 | dependency: "direct main" 163 | description: flutter 164 | source: sdk 165 | version: "0.0.0" 166 | flutter_sinusoidals_v2: 167 | dependency: "direct main" 168 | description: 169 | name: flutter_sinusoidals_v2 170 | sha256: e08718ad18ff2c820f3f4fdabd069694219c5c829ddb53c256592261df3d69fc 171 | url: "https://pub.flutter-io.cn" 172 | source: hosted 173 | version: "0.2.3" 174 | flutter_test: 175 | dependency: "direct dev" 176 | description: flutter 177 | source: sdk 178 | version: "0.0.0" 179 | flutter_web_plugins: 180 | dependency: transitive 181 | description: flutter 182 | source: sdk 183 | version: "0.0.0" 184 | http: 185 | dependency: transitive 186 | description: 187 | name: http 188 | sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f 189 | url: "https://pub.flutter-io.cn" 190 | source: hosted 191 | version: "1.3.0" 192 | http_parser: 193 | dependency: transitive 194 | description: 195 | name: http_parser 196 | sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" 197 | url: "https://pub.flutter-io.cn" 198 | source: hosted 199 | version: "4.1.2" 200 | image: 201 | dependency: transitive 202 | description: 203 | name: image 204 | sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" 205 | url: "https://pub.flutter-io.cn" 206 | source: hosted 207 | version: "4.5.4" 208 | intl: 209 | dependency: "direct main" 210 | description: 211 | name: intl 212 | sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf 213 | url: "https://pub.flutter-io.cn" 214 | source: hosted 215 | version: "0.19.0" 216 | json_annotation: 217 | dependency: transitive 218 | description: 219 | name: json_annotation 220 | sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" 221 | url: "https://pub.flutter-io.cn" 222 | source: hosted 223 | version: "4.9.0" 224 | leak_tracker: 225 | dependency: transitive 226 | description: 227 | name: leak_tracker 228 | sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec 229 | url: "https://pub.flutter-io.cn" 230 | source: hosted 231 | version: "10.0.8" 232 | leak_tracker_flutter_testing: 233 | dependency: transitive 234 | description: 235 | name: leak_tracker_flutter_testing 236 | sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 237 | url: "https://pub.flutter-io.cn" 238 | source: hosted 239 | version: "3.0.9" 240 | leak_tracker_testing: 241 | dependency: transitive 242 | description: 243 | name: leak_tracker_testing 244 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 245 | url: "https://pub.flutter-io.cn" 246 | source: hosted 247 | version: "3.0.1" 248 | lints: 249 | dependency: transitive 250 | description: 251 | name: lints 252 | sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 253 | url: "https://pub.flutter-io.cn" 254 | source: hosted 255 | version: "5.1.1" 256 | matcher: 257 | dependency: transitive 258 | description: 259 | name: matcher 260 | sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 261 | url: "https://pub.flutter-io.cn" 262 | source: hosted 263 | version: "0.12.17" 264 | material_color_utilities: 265 | dependency: transitive 266 | description: 267 | name: material_color_utilities 268 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 269 | url: "https://pub.flutter-io.cn" 270 | source: hosted 271 | version: "0.11.1" 272 | meta: 273 | dependency: transitive 274 | description: 275 | name: meta 276 | sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c 277 | url: "https://pub.flutter-io.cn" 278 | source: hosted 279 | version: "1.16.0" 280 | nested: 281 | dependency: transitive 282 | description: 283 | name: nested 284 | sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 285 | url: "https://pub.flutter-io.cn" 286 | source: hosted 287 | version: "1.0.0" 288 | package_info_plus: 289 | dependency: "direct main" 290 | description: 291 | name: package_info_plus 292 | sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" 293 | url: "https://pub.flutter-io.cn" 294 | source: hosted 295 | version: "8.3.0" 296 | package_info_plus_platform_interface: 297 | dependency: transitive 298 | description: 299 | name: package_info_plus_platform_interface 300 | sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" 301 | url: "https://pub.flutter-io.cn" 302 | source: hosted 303 | version: "3.2.0" 304 | path: 305 | dependency: transitive 306 | description: 307 | name: path 308 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 309 | url: "https://pub.flutter-io.cn" 310 | source: hosted 311 | version: "1.9.1" 312 | path_provider_linux: 313 | dependency: transitive 314 | description: 315 | name: path_provider_linux 316 | sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 317 | url: "https://pub.flutter-io.cn" 318 | source: hosted 319 | version: "2.2.1" 320 | path_provider_platform_interface: 321 | dependency: transitive 322 | description: 323 | name: path_provider_platform_interface 324 | sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" 325 | url: "https://pub.flutter-io.cn" 326 | source: hosted 327 | version: "2.1.2" 328 | path_provider_windows: 329 | dependency: transitive 330 | description: 331 | name: path_provider_windows 332 | sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 333 | url: "https://pub.flutter-io.cn" 334 | source: hosted 335 | version: "2.3.0" 336 | petitparser: 337 | dependency: transitive 338 | description: 339 | name: petitparser 340 | sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" 341 | url: "https://pub.flutter-io.cn" 342 | source: hosted 343 | version: "6.1.0" 344 | platform: 345 | dependency: transitive 346 | description: 347 | name: platform 348 | sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" 349 | url: "https://pub.flutter-io.cn" 350 | source: hosted 351 | version: "3.1.6" 352 | plugin_platform_interface: 353 | dependency: transitive 354 | description: 355 | name: plugin_platform_interface 356 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 357 | url: "https://pub.flutter-io.cn" 358 | source: hosted 359 | version: "2.1.8" 360 | posix: 361 | dependency: transitive 362 | description: 363 | name: posix 364 | sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 365 | url: "https://pub.flutter-io.cn" 366 | source: hosted 367 | version: "6.0.2" 368 | provider: 369 | dependency: "direct main" 370 | description: 371 | name: provider 372 | sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310" 373 | url: "https://pub.flutter-io.cn" 374 | source: hosted 375 | version: "6.1.4" 376 | real_volume: 377 | dependency: "direct main" 378 | description: 379 | name: real_volume 380 | sha256: "77e7dfb900e1cb73f97b0cdbfd494b43a336dce5547245a9a827701079b09709" 381 | url: "https://pub.flutter-io.cn" 382 | source: hosted 383 | version: "1.0.9" 384 | shared_preferences: 385 | dependency: "direct main" 386 | description: 387 | name: shared_preferences 388 | sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" 389 | url: "https://pub.flutter-io.cn" 390 | source: hosted 391 | version: "2.5.3" 392 | shared_preferences_android: 393 | dependency: transitive 394 | description: 395 | name: shared_preferences_android 396 | sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" 397 | url: "https://pub.flutter-io.cn" 398 | source: hosted 399 | version: "2.4.10" 400 | shared_preferences_foundation: 401 | dependency: transitive 402 | description: 403 | name: shared_preferences_foundation 404 | sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" 405 | url: "https://pub.flutter-io.cn" 406 | source: hosted 407 | version: "2.5.4" 408 | shared_preferences_linux: 409 | dependency: transitive 410 | description: 411 | name: shared_preferences_linux 412 | sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" 413 | url: "https://pub.flutter-io.cn" 414 | source: hosted 415 | version: "2.4.1" 416 | shared_preferences_platform_interface: 417 | dependency: transitive 418 | description: 419 | name: shared_preferences_platform_interface 420 | sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" 421 | url: "https://pub.flutter-io.cn" 422 | source: hosted 423 | version: "2.4.1" 424 | shared_preferences_web: 425 | dependency: transitive 426 | description: 427 | name: shared_preferences_web 428 | sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 429 | url: "https://pub.flutter-io.cn" 430 | source: hosted 431 | version: "2.4.3" 432 | shared_preferences_windows: 433 | dependency: transitive 434 | description: 435 | name: shared_preferences_windows 436 | sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" 437 | url: "https://pub.flutter-io.cn" 438 | source: hosted 439 | version: "2.4.1" 440 | sky_engine: 441 | dependency: transitive 442 | description: flutter 443 | source: sdk 444 | version: "0.0.0" 445 | sound_generator: 446 | dependency: "direct main" 447 | description: 448 | path: "." 449 | ref: HEAD 450 | resolved-ref: "4725ef6d1bbcbc80af152c0e03c45dccbc006360" 451 | url: "https://github.com/BHznJNs/sound_generator.git" 452 | source: git 453 | version: "0.0.13" 454 | source_span: 455 | dependency: transitive 456 | description: 457 | name: source_span 458 | sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" 459 | url: "https://pub.flutter-io.cn" 460 | source: hosted 461 | version: "1.10.1" 462 | stack_trace: 463 | dependency: transitive 464 | description: 465 | name: stack_trace 466 | sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" 467 | url: "https://pub.flutter-io.cn" 468 | source: hosted 469 | version: "1.12.1" 470 | stream_channel: 471 | dependency: transitive 472 | description: 473 | name: stream_channel 474 | sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" 475 | url: "https://pub.flutter-io.cn" 476 | source: hosted 477 | version: "2.1.4" 478 | string_scanner: 479 | dependency: transitive 480 | description: 481 | name: string_scanner 482 | sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" 483 | url: "https://pub.flutter-io.cn" 484 | source: hosted 485 | version: "1.4.1" 486 | term_glyph: 487 | dependency: transitive 488 | description: 489 | name: term_glyph 490 | sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" 491 | url: "https://pub.flutter-io.cn" 492 | source: hosted 493 | version: "1.2.2" 494 | test_api: 495 | dependency: transitive 496 | description: 497 | name: test_api 498 | sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd 499 | url: "https://pub.flutter-io.cn" 500 | source: hosted 501 | version: "0.7.4" 502 | typed_data: 503 | dependency: transitive 504 | description: 505 | name: typed_data 506 | sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 507 | url: "https://pub.flutter-io.cn" 508 | source: hosted 509 | version: "1.4.0" 510 | url_launcher: 511 | dependency: "direct main" 512 | description: 513 | name: url_launcher 514 | sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" 515 | url: "https://pub.flutter-io.cn" 516 | source: hosted 517 | version: "6.3.1" 518 | url_launcher_android: 519 | dependency: transitive 520 | description: 521 | name: url_launcher_android 522 | sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" 523 | url: "https://pub.flutter-io.cn" 524 | source: hosted 525 | version: "6.3.16" 526 | url_launcher_ios: 527 | dependency: transitive 528 | description: 529 | name: url_launcher_ios 530 | sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" 531 | url: "https://pub.flutter-io.cn" 532 | source: hosted 533 | version: "6.3.3" 534 | url_launcher_linux: 535 | dependency: transitive 536 | description: 537 | name: url_launcher_linux 538 | sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" 539 | url: "https://pub.flutter-io.cn" 540 | source: hosted 541 | version: "3.2.1" 542 | url_launcher_macos: 543 | dependency: transitive 544 | description: 545 | name: url_launcher_macos 546 | sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" 547 | url: "https://pub.flutter-io.cn" 548 | source: hosted 549 | version: "3.2.2" 550 | url_launcher_platform_interface: 551 | dependency: transitive 552 | description: 553 | name: url_launcher_platform_interface 554 | sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" 555 | url: "https://pub.flutter-io.cn" 556 | source: hosted 557 | version: "2.3.2" 558 | url_launcher_web: 559 | dependency: transitive 560 | description: 561 | name: url_launcher_web 562 | sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" 563 | url: "https://pub.flutter-io.cn" 564 | source: hosted 565 | version: "2.4.1" 566 | url_launcher_windows: 567 | dependency: transitive 568 | description: 569 | name: url_launcher_windows 570 | sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" 571 | url: "https://pub.flutter-io.cn" 572 | source: hosted 573 | version: "3.1.4" 574 | vector_math: 575 | dependency: transitive 576 | description: 577 | name: vector_math 578 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 579 | url: "https://pub.flutter-io.cn" 580 | source: hosted 581 | version: "2.1.4" 582 | vm_service: 583 | dependency: transitive 584 | description: 585 | name: vm_service 586 | sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" 587 | url: "https://pub.flutter-io.cn" 588 | source: hosted 589 | version: "14.3.1" 590 | web: 591 | dependency: transitive 592 | description: 593 | name: web 594 | sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" 595 | url: "https://pub.flutter-io.cn" 596 | source: hosted 597 | version: "1.1.1" 598 | win32: 599 | dependency: transitive 600 | description: 601 | name: win32 602 | sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f 603 | url: "https://pub.flutter-io.cn" 604 | source: hosted 605 | version: "5.12.0" 606 | xdg_directories: 607 | dependency: transitive 608 | description: 609 | name: xdg_directories 610 | sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" 611 | url: "https://pub.flutter-io.cn" 612 | source: hosted 613 | version: "1.1.0" 614 | xml: 615 | dependency: transitive 616 | description: 617 | name: xml 618 | sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 619 | url: "https://pub.flutter-io.cn" 620 | source: hosted 621 | version: "6.5.0" 622 | yaml: 623 | dependency: transitive 624 | description: 625 | name: yaml 626 | sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce 627 | url: "https://pub.flutter-io.cn" 628 | source: hosted 629 | version: "3.1.3" 630 | sdks: 631 | dart: ">=3.7.2 <4.0.0" 632 | flutter: ">=3.29.3" 633 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: motion_ease_tune 2 | description: "A new Flutter project." 3 | # The following line prevents the package from being accidentally published to 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 5 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 6 | 7 | # The following defines the version and build number for your application. 8 | # A version number is three numbers separated by dots, like 1.2.43 9 | # followed by an optional build number separated by a +. 10 | # Both the version and the builder number may be overridden in flutter 11 | # build by specifying --build-name and --build-number, respectively. 12 | # In Android, build-name is used as versionName while build-number used as versionCode. 13 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 14 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 15 | # Read more about iOS versioning at 16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 17 | # In Windows, build-name is used as the major, minor, and patch parts 18 | # of the product and file versions while build-number is used as the build suffix. 19 | version: 1.1.3+9 20 | 21 | environment: 22 | sdk: ^3.7.2 23 | flutter: ^3.29.3 24 | 25 | # Dependencies specify other packages that your package needs in order to work. 26 | # To automatically upgrade your package dependencies to the latest versions 27 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 28 | # dependencies can be manually updated by changing the version numbers below to 29 | # the latest version available on pub.dev. To see which dependencies have newer 30 | # versions available, run `flutter pub outdated`. 31 | dependencies: 32 | flutter: 33 | sdk: flutter 34 | 35 | flutter_localizations: 36 | sdk: flutter 37 | intl: any 38 | 39 | audio_video_progress_bar: ^2.0.3 40 | dynamic_color: ^1.7.0 41 | flutter_sinusoidals_v2: ^0.2.3 42 | package_info_plus: ^8.3.0 43 | provider: ^6.1.4 44 | real_volume: ^1.0.9 45 | url_launcher: ^6.3.1 46 | shared_preferences: ^2.5.3 47 | sound_generator: 48 | git: 49 | https://github.com/BHznJNs/sound_generator.git 50 | 51 | # The following adds the Cupertino Icons font to your application. 52 | # Use with the CupertinoIcons class for iOS style icons. 53 | cupertino_icons: ^1.0.8 54 | 55 | dev_dependencies: 56 | flutter_test: 57 | sdk: flutter 58 | 59 | flutter_launcher_icons: "^0.14.3" 60 | # The "flutter_lints" package below contains a set of recommended lints to 61 | # encourage good coding practices. The lint set provided by the package is 62 | # activated in the `analysis_options.yaml` file located at the root of your 63 | # package. See that file for information about deactivating specific lint 64 | # rules and activating additional ones. 65 | flutter_lints: ^5.0.0 66 | 67 | # For information on the generic Dart part of this file, see the 68 | # following page: https://dart.dev/tools/pub/pubspec 69 | 70 | # The following section is specific to Flutter packages. 71 | flutter: 72 | generate: true 73 | 74 | # The following line ensures that the Material Icons font is 75 | # included with your application, so that you can use the icons in 76 | # the material Icons class. 77 | uses-material-design: true 78 | 79 | assets: 80 | - assets/icon/ 81 | - assets/guide/ 82 | 83 | fonts: 84 | - family: ComicRelief 85 | fonts: 86 | - asset: assets/ComicRelief/ComicRelief.ttf 87 | 88 | flutter_launcher_icons: 89 | android: "ic_launcher" 90 | ios: false 91 | image_path: "assets/icon/icon.png" 92 | min_sdk_android: 21 93 | -------------------------------------------------------------------------------- /screenshots/main_view.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/screenshots/main_view.jpg -------------------------------------------------------------------------------- /screenshots/user_guide_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/screenshots/user_guide_1.jpg -------------------------------------------------------------------------------- /screenshots/user_guide_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/screenshots/user_guide_2.jpg -------------------------------------------------------------------------------- /screenshots/user_guide_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BHznJNs/MotionEaseTune/cc574c7a624b15741b06c2c60a9bb7892bd4cee4/screenshots/user_guide_3.jpg -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:motion_ease_tune/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const AppEntry()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | --------------------------------------------------------------------------------