├── .github └── workflows │ ├── build.yml │ └── verify_and_test.yml ├── .gitignore ├── .gitmodules ├── .metadata ├── .run └── Run.run.xml ├── .vscode └── launch.json ├── COPYING ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle.kts │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── infusion_timer │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-mdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable-xhdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xxhdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xxxhdpi │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable │ │ │ ├── launch_background.xml │ │ │ └── notification_icon.png │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.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 │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle.kts ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── key.properties.here └── settings.gradle.kts ├── appstream ├── de.sesu8642.infusion_timer.appdata.xml └── de.sesu8642.infusion_timer.desktop ├── assets ├── default_data.json ├── feature graphic.xcf ├── hand-bell-ringing-sound.txt ├── hand-bell-ringing-sound.wav ├── icon_simple.svg ├── icon_simple_512.png ├── icon_simple_512_extra_padding.png ├── icon_simple_colored.svg ├── icon_simple_colored_512.png └── icon_simple_padding.png ├── build_release_android.sh ├── build_release_linux.sh ├── build_release_web.sh ├── build_release_windows.bat ├── ci_files └── keystore-github-ci.jks ├── flutter_launcher_icons.yaml ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h └── RunnerTests │ └── RunnerTests.swift ├── lib ├── additional_license_factory.dart ├── backup_data.dart ├── id_generator.dart ├── main.dart ├── persistence_service.dart ├── tea.dart └── widgets │ ├── collection_page.dart │ ├── confirm_dialog.dart │ ├── data_backup_dialog.dart │ ├── data_restore_dialog.dart │ ├── notes_page.dart │ ├── preferences_page.dart │ ├── star_rating_form_field.dart │ ├── tea_actions_bottom_sheet.dart │ ├── tea_card.dart │ ├── tea_input_dialog.dart │ └── timer_page.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake └── runner │ ├── CMakeLists.txt │ ├── main.cc │ ├── my_application.cc │ └── my_application.h ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── app_icon_1024.png │ │ │ ├── app_icon_128.png │ │ │ ├── app_icon_16.png │ │ │ ├── app_icon_256.png │ │ │ ├── app_icon_32.png │ │ │ ├── app_icon_512.png │ │ │ └── app_icon_64.png │ ├── Base.lproj │ │ └── MainMenu.xib │ ├── Configs │ │ ├── AppInfo.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements └── RunnerTests │ └── RunnerTests.swift ├── metadata ├── de-DE │ ├── full_description.txt │ └── short_description.txt └── en-US │ ├── changelogs │ ├── 100.txt │ ├── 101.txt │ ├── 102.txt │ ├── 103.txt │ ├── 104.txt │ ├── 105.txt │ ├── 106.txt │ ├── 107.txt │ ├── 108.txt │ ├── 109.txt │ ├── 110.txt │ ├── 111.txt │ ├── 112.txt │ └── 113.txt │ ├── full_description.txt │ ├── images │ ├── featureGraphic.png │ ├── icon.png │ ├── phoneScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ └── 3.png │ └── sevenInchScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ └── 3.png │ ├── short_description.txt │ └── title.txt ├── pubspec.lock ├── pubspec.yaml ├── test ├── backup_data_validation_test.dart └── tea_validation_test.dart ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── index.html └── manifest.json └── windows ├── .gitignore ├── CMakeLists.txt ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake └── runner ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | 11 | build_android: 12 | runs-on: ubuntu-latest 13 | container: 14 | # trying to be close to the f-droid build to detect failures early 15 | image: registry.gitlab.com/fdroid/fdroidserver:buildserver-bookworm 16 | env: 17 | ANDROID_HOME: /opt/android-sdk 18 | steps: 19 | 20 | - name: apt update 21 | run: sudo apt update 22 | 23 | - name: Install jq 24 | run: sudo apt install jq 25 | 26 | - name: Install xz-utils 27 | run: sudo apt install xz-utils 28 | 29 | - name: Check out repository code 30 | uses: actions/checkout@v3 31 | with: 32 | submodules: recursive 33 | 34 | # Tags are required for Flutter to know its own version and check constraints 35 | - run: git -C ./flutter fetch --tags 36 | 37 | - run: ./flutter/bin/flutter config --no-analytics 38 | 39 | - run: ./flutter/bin/flutter --version 40 | 41 | - name: Build Android APK 42 | env: 43 | SIGNING_KEY_PWD: ${{ secrets.SIGNING_KEY_PWD }} 44 | run: ./flutter/bin/flutter build apk --flavor ci 45 | 46 | - name: Upload Android apk 47 | uses: actions/upload-artifact@v4 48 | with: 49 | name: Android apk 50 | path: ./build/app/outputs/flutter-apk/app-ci-release.apk 51 | -------------------------------------------------------------------------------- /.github/workflows/verify_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Verify and test 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | submodules: recursive 17 | 18 | # Tags are required for Flutter to know its own version and check constraints 19 | - run: git -C ./flutter fetch --tags 20 | 21 | - run: ./flutter/bin/flutter config --no-analytics 22 | 23 | - name: Install dependencies 24 | run: ./flutter/bin/flutter pub get 25 | 26 | - name: Verify formatting 27 | run: ./flutter/bin/dart format --output=none --set-exit-if-changed ./lib 28 | 29 | - name: Analyze project source 30 | run: ./flutter/bin/flutter analyze ./lib 31 | 32 | - name: Run tests 33 | run: ./flutter/bin/flutter test -v 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | /release 46 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "flutter"] 2 | path = flutter 3 | url = https://github.com/flutter/flutter 4 | -------------------------------------------------------------------------------- /.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: 1d9032c7e1d867f071f2277eb1673e8f9b0274e3 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /.run/Run.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "infusion_timer (debug mode)", 9 | "program": "lib/main.dart", 10 | "request": "launch", 11 | "type": "dart", 12 | "flutterMode": "debug", 13 | "args": ["--flavor", "ci"] 14 | }, 15 | { 16 | "name": "infusion_timer (profile mode)", 17 | "program": "lib/main.dart", 18 | "request": "launch", 19 | "type": "dart", 20 | "flutterMode": "profile", 21 | "args": ["--flavor", "ci"] 22 | }, 23 | { 24 | "name": "infusion_timer (release mode)", 25 | "program": "lib/main.dart", 26 | "request": "launch", 27 | "type": "dart", 28 | "flutterMode": "release", 29 | "args": ["--flavor", "prod"] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Enthusiast Tea Timer 2 | 3 | Effortlessly organize your tea collection, complete with infusion times and brewing instructions tailored to your taste - whether you prefer Gong Fu Cha or Western style. Enjoy handy presets for popular tea varieties. Take tasting notes, and give star ratings to your teas. Best of all, it's free, open-source, and designed with your privacy in mind - no ads, no data collection. Cheers to your perfect cup! 4 | 5 | [Get it on F-Droid](https://f-droid.org/packages/com.sesu8642.infusion_timer/) 8 | [Get it on Google Play](https://play.google.com/store/apps/details?id=com.sesu8642.infusion_timer) 11 | 12 | ## Screenshots 13 | ![Tea Collection](metadata/en-US/images/phoneScreenshots/1.png) 14 | ![Tea Timer](metadata/en-US/images/phoneScreenshots/2.png) 15 | ![Preferences](metadata/en-US/images/phoneScreenshots/3.png) 16 | 17 | ## Troubleshooting 18 | 19 | If the countdown is much faster than expected, it's probably connected to a UI speed. It occurred at least with GrapheneOS. Other alternative android builds may be problematic as well. 20 | 21 | ### 1. Enable Developer mode 22 | 1. Tap on the "Settings" (gear icon) icon to open the setting on your Android device. 23 | 2. On the Android Settings screen, tap on "About phone" option (sometimes also "System" > "Advanced" > "About Phone"). 24 | 3. Start tapping on the "Build number" option, and you will see a pop-up message that you are now "X" steps away from being a developer. Continue tapping on the 4. Build number option until you see a screen to Re-enter your password. Once you enter your device password, it will show a message at the bottom of the screen: You are now a developer. 25 | 26 | ### 2. Fix the UI-related bug 27 | 1. Go to the "Settings" -> "System" -> "Developer options". 28 | 2. Switch the "Window animation scale" and "Transition animation scale" from "Animation off" to "Animation scale 1x," then put them back to "Animation off." 29 | 30 | 31 | ## Licenses 32 | Copyright (c) 2021-2024 Sesu8642 33 | 34 | Unless otherwise specified, source code in this repository is licensed under the GNU General Public License, Version 3 or later (GPL-3.0-or-later). A copy is included in the COPYING file. 35 | 36 | Assets in this repository are licensed under various other licenses: 37 | 38 | - **assets/icon_simple***: Gaiwan Icon © 2021-2024 by [Sesu8642](https://github.com/sesu8642) is licensed under [CC BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/?ref=chooser-v1) 39 | - **assets/hand-bell-ringing-sound.wav**: CC BY 4.0 (see assets/hand-bell-ringing-sound.txt) 40 | - **assets/default_data.json**: copyright meileaf 2016 41 | 42 | ## Creating Release Artifacts 43 | ### Android 44 | - create a file key.properties in /android/key.properties (template should be there) 45 | - run build_release_android.sh 46 | 47 | ### Windows 48 | - see [the official instructions](https://docs.flutter.dev/get-started/install/windows/desktop) 49 | - install legacy media player feature if you have the N version of Windows 50 | - run build_release_windows.bat -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | plugins { 5 | id("com.android.application") 6 | id("kotlin-android") 7 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 8 | id("dev.flutter.flutter-gradle-plugin") 9 | } 10 | 11 | android { 12 | namespace = "com.sesu8642.infusion_timer" 13 | compileSdk = flutter.compileSdkVersion 14 | ndkVersion = flutter.ndkVersion 15 | 16 | compileOptions { 17 | sourceCompatibility = JavaVersion.VERSION_11 18 | targetCompatibility = JavaVersion.VERSION_11 19 | // for flutter_local_notifications 20 | isCoreLibraryDesugaringEnabled = true 21 | } 22 | 23 | kotlinOptions { 24 | jvmTarget = JavaVersion.VERSION_11.toString() 25 | } 26 | 27 | defaultConfig { 28 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 29 | applicationId = "com.sesu8642.infusion_timer" 30 | // You can update the following values to match your application needs. 31 | // For more information, see: https://flutter.dev/to/review-gradle-config. 32 | minSdk = flutter.minSdkVersion 33 | targetSdk = flutter.targetSdkVersion 34 | versionCode = flutter.versionCode 35 | versionName = flutter.versionName 36 | // for flutter_local_notifications 37 | multiDexEnabled = true 38 | } 39 | 40 | buildTypes { 41 | release { 42 | // TODO: Add your own signing config for the release build. 43 | // nope, its in the flavors 44 | } 45 | } 46 | 47 | val keystoreProperties = Properties().apply { 48 | val keystorePropertiesFile = rootProject.file("key.properties") 49 | if (keystorePropertiesFile.exists()) { 50 | load(FileInputStream(keystorePropertiesFile)) 51 | } 52 | } 53 | 54 | signingConfigs { 55 | create("release") { 56 | keyAlias = keystoreProperties["keyAlias"] as? String ?: "" 57 | keyPassword = keystoreProperties["keyPassword"] as? String ?: "" 58 | storeFile = keystoreProperties["storeFile"]?.let { file(it) } 59 | storePassword = keystoreProperties["storePassword"] as? String ?: "" 60 | } 61 | create("ci") { 62 | keyAlias = "upload" 63 | keyPassword = System.getenv("SIGNING_KEY_PWD") 64 | storeFile = file("../../ci_files/keystore-github-ci.jks") 65 | storePassword = System.getenv("SIGNING_KEY_PWD") 66 | } 67 | } 68 | 69 | flavorDimensions.add("Signing") 70 | 71 | productFlavors { 72 | create("default") { 73 | signingConfig = signingConfigs.getByName("release") 74 | } 75 | create("ci") { 76 | signingConfig = signingConfigs.getByName("ci") 77 | applicationIdSuffix = ".ci" 78 | versionNameSuffix = "-ci" 79 | } 80 | } 81 | 82 | } 83 | 84 | flutter { 85 | source = "../.." 86 | } 87 | 88 | dependencies { 89 | // for flutter_local_notifications 90 | coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") 91 | } -------------------------------------------------------------------------------- /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 | 38 | 41 | 49 | 50 | 54 | 55 | 56 | 58 | 59 | 60 | 61 | 63 | 64 | 65 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/infusion_timer/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.sesu8642.infusion_timer 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity : FlutterActivity() 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /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-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/android/app/src/main/res/drawable/notification_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/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/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #009688 4 | -------------------------------------------------------------------------------- /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 = 9 | rootProject.layout.buildDirectory 10 | .dir("../../build") 11 | .get() 12 | rootProject.layout.buildDirectory.value(newBuildDir) 13 | 14 | subprojects { 15 | val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) 16 | project.layout.buildDirectory.value(newSubprojectBuildDir) 17 | } 18 | subprojects { 19 | project.evaluationDependsOn(":app") 20 | } 21 | 22 | tasks.register("clean") { 23 | delete(rootProject.layout.buildDirectory) 24 | } 25 | -------------------------------------------------------------------------------- /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.12-all.zip 6 | -------------------------------------------------------------------------------- /android/key.properties.here: -------------------------------------------------------------------------------- 1 | storePassword= 2 | keyPassword= 3 | keyAlias=upload 4 | storeFile=/upload-keystore.jks> 5 | -------------------------------------------------------------------------------- /android/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | val flutterSdkPath = 3 | run { 4 | val properties = java.util.Properties() 5 | file("local.properties").inputStream().use { properties.load(it) } 6 | val flutterSdkPath = properties.getProperty("flutter.sdk") 7 | require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } 8 | flutterSdkPath 9 | } 10 | 11 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | } 19 | 20 | plugins { 21 | id("dev.flutter.flutter-plugin-loader") version "1.0.0" 22 | id("com.android.application") version "8.9.1" apply false 23 | id("org.jetbrains.kotlin.android") version "2.1.0" apply false 24 | } 25 | 26 | include(":app") 27 | -------------------------------------------------------------------------------- /appstream/de.sesu8642.infusion_timer.appdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | de.sesu8642.infusion_timer 5 | CC0-1.0 6 | GPL-3.0-or-later 7 | Enthusiast Tea Timer 8 | 9 | Sesu8642 10 | contact_AT_sesu8642.de 11 | 12 | Tea timer for Gong Fu and western style brewing for true enthusiasts. 13 | Tee-Timer für Gong Fu oder westliche Teezubereitung für echte Enthusiasten. 14 | 15 | 16 |

Effortlessly organize your tea collection, complete with infusion times and brewing 17 | instructions tailored to your taste - whether you prefer Gong Fu Cha or Western style. Enjoy 18 | handy presets for popular tea varieties. Take tasting notes, and give star ratings to your 19 | teas. Best of all, it's free, open-source, and designed with your privacy in mind - no ads, no 20 | data collection. Cheers to your perfect cup!

21 |
22 | 23 |

Verwalte bequem Deine Teesammlung. Füge ganz nach Deinem Geschmack Aufgusszeiten und 24 | Zubereitungsanweisungen hinzu - sei es Gong Fu Cha oder westliche Teezubereitung. Für die 25 | gängigsten Teesorten gibt es praktische Voreinstellungen. Halte deine Erfahrungen beim 26 | Teegenuss in Verkostungsnotizen fest und vergib Sternebewertungen. Das Beste daran: Es ist 27 | Frei, Open Source und von Grund auf privatsphärefreundlich konzipiert. Keine Werbung, keine 28 | Datensammlung. Auf die perfekte Tasse Tee!

29 |
30 | 31 | de.sesu8642.infusion_timer.desktop 32 | 33 | 34 | 35 | 36 | https://raw.githubusercontent.com/sesu8642/InfusionTimer/master/metadata/en-US/images/featureGraphic.png 37 | 38 | 39 | 40 | https://raw.githubusercontent.com/sesu8642/InfusionTimer/master/metadata/en-US/images/sevenInchScreenshots/1.png 41 | 42 | 43 | 44 | https://raw.githubusercontent.com/sesu8642/InfusionTimer/master/metadata/en-US/images/sevenInchScreenshots/2.png 45 | 46 | 47 | 48 | https://raw.githubusercontent.com/sesu8642/InfusionTimer/master/metadata/en-US/images/sevenInchScreenshots/3.png 49 | 50 | 51 | 52 | 53 | 54 | https://github.com/Sesu8642/InfusionTimer 55 | https://github.com/Sesu8642/InfusionTimer/issues 56 | 57 | 58 | 59 | 60 | https://raw.githubusercontent.com/Sesu8642/InfusionTimer/master/metadata/en-US/changelogs/113.txt 61 | 62 | 63 | 64 | https://raw.githubusercontent.com/Sesu8642/InfusionTimer/master/metadata/en-US/changelogs/112.txt 65 | 66 | 67 | 68 | https://raw.githubusercontent.com/Sesu8642/InfusionTimer/master/metadata/en-US/changelogs/111.txt 69 | 70 | 71 | 72 | https://raw.githubusercontent.com/Sesu8642/InfusionTimer/master/metadata/en-US/changelogs/110.txt 73 | 74 | 75 |
-------------------------------------------------------------------------------- /appstream/de.sesu8642.infusion_timer.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=Enthusiast Tea Timer 4 | Comment=Tea timer for Gong Fu and western style brewing for true enthusiasts. 5 | Comment[de]=Tee-Timer-App für Gong Fu oder westliche Teezubereitung für echte Enthusiasten. 6 | Icon=de.sesu8642.infusion_timer 7 | Exec=enthusiast_tea_timer 8 | Categories=Utility;Clock; 9 | Keywords=Utility;Tea Timer 10 | Keywords[de]=Utility;Tee-Timer 11 | -------------------------------------------------------------------------------- /assets/default_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "White Tea", 4 | "temperature": 85, 5 | "gPer100Ml": 0.6, 6 | "infusions": [ 7 | { 8 | "duration": 180 9 | }, 10 | { 11 | "duration": 240 12 | } 13 | ], 14 | "subtitle": "western style brewing" 15 | }, 16 | { 17 | "name": "Green Tea", 18 | "temperature": 80, 19 | "gPer100Ml": 0.5, 20 | "infusions": [ 21 | { 22 | "duration": 120 23 | }, 24 | { 25 | "duration": 180 26 | } 27 | ], 28 | "subtitle": "western style brewing" 29 | }, 30 | { 31 | "name": "Oolong Tea (strip)", 32 | "temperature": 99, 33 | "gPer100Ml": 0.8, 34 | "infusions": [ 35 | { 36 | "duration": 120 37 | }, 38 | { 39 | "duration": 150 40 | }, 41 | { 42 | "duration": 180 43 | }, 44 | { 45 | "duration": 210 46 | } 47 | ], 48 | "subtitle": "western style brewing" 49 | }, 50 | { 51 | "name": "Oolong Tea (ball)", 52 | "temperature": 99, 53 | "gPer100Ml": 1, 54 | "infusions": [ 55 | { 56 | "duration": 120 57 | }, 58 | { 59 | "duration": 150 60 | }, 61 | { 62 | "duration": 180 63 | }, 64 | { 65 | "duration": 210 66 | }, 67 | { 68 | "duration": 240 69 | } 70 | ], 71 | "subtitle": "western style brewing" 72 | }, 73 | { 74 | "name": "Black Tea (small leaf)", 75 | "temperature": 90, 76 | "gPer100Ml": 0.8, 77 | "infusions": [ 78 | { 79 | "duration": 120 80 | }, 81 | { 82 | "duration": 180 83 | }, 84 | { 85 | "duration": 240 86 | }, 87 | { 88 | "duration": 320 89 | } 90 | ], 91 | "subtitle": "western style brewing" 92 | }, 93 | { 94 | "name": "Black Tea (large leaf)", 95 | "temperature": 95, 96 | "gPer100Ml": 0.7, 97 | "infusions": [ 98 | { 99 | "duration": 120 100 | }, 101 | { 102 | "duration": 180 103 | }, 104 | { 105 | "duration": 240 106 | }, 107 | { 108 | "duration": 320 109 | } 110 | ], 111 | "subtitle": "western style brewing" 112 | }, 113 | { 114 | "name": "PuErh Tea (raw)", 115 | "temperature": 95, 116 | "gPer100Ml": 0.9, 117 | "infusions": [ 118 | { 119 | "duration": 120 120 | }, 121 | { 122 | "duration": 150 123 | }, 124 | { 125 | "duration": 180 126 | }, 127 | { 128 | "duration": 210 129 | }, 130 | { 131 | "duration": 240 132 | }, 133 | { 134 | "duration": 270 135 | } 136 | ], 137 | "subtitle": "western style brewing" 138 | }, 139 | { 140 | "name": "PuErh Tea (ripe)", 141 | "temperature": 99, 142 | "gPer100Ml": 0.9, 143 | "infusions": [ 144 | { 145 | "duration": 120 146 | }, 147 | { 148 | "duration": 150 149 | }, 150 | { 151 | "duration": 180 152 | }, 153 | { 154 | "duration": 210 155 | }, 156 | { 157 | "duration": 240 158 | }, 159 | { 160 | "duration": 270 161 | } 162 | ], 163 | "subtitle": "western style brewing" 164 | } 165 | ] 166 | -------------------------------------------------------------------------------- /assets/feature graphic.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/assets/feature graphic.xcf -------------------------------------------------------------------------------- /assets/hand-bell-ringing-sound.txt: -------------------------------------------------------------------------------- 1 | 2 | "Free Sounds Library" 3 | 4 | 5 | Free Sound Effects Site. 6 | 7 | 8 | Licence: License: Attribution 4.0 International (CC BY 4.0). You are allowed to use sound effects free of charge and royalty free in your multimedia projects for commercial or non-commercial purposes. 9 | 10 | 11 | http://www.freesoundslibrary.com 12 | 13 | https://www.freesoundslibrary.com/hand-bell-ringing-sound/ 14 | BY SPANAC 15 | 16 | converted to wav and cut by Sesu8642 using ffmpeg 17 | -------------------------------------------------------------------------------- /assets/hand-bell-ringing-sound.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/assets/hand-bell-ringing-sound.wav -------------------------------------------------------------------------------- /assets/icon_simple.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 39 | 52 | 69 | 82 | 83 | 107 | 109 | 110 | 112 | image/svg+xml 113 | 115 | 116 | 117 | 118 | 119 | 125 | 128 | 136 | 144 | 149 | 150 | 151 | 200 | 201 | -------------------------------------------------------------------------------- /assets/icon_simple_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/assets/icon_simple_512.png -------------------------------------------------------------------------------- /assets/icon_simple_512_extra_padding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/assets/icon_simple_512_extra_padding.png -------------------------------------------------------------------------------- /assets/icon_simple_colored_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/assets/icon_simple_colored_512.png -------------------------------------------------------------------------------- /assets/icon_simple_padding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/assets/icon_simple_padding.png -------------------------------------------------------------------------------- /build_release_android.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | flutterBin=flutter/bin/flutter 4 | 5 | mkdir -p ./release 6 | $flutterBin clean 7 | 8 | $flutterBin build appbundle --release 9 | $flutterBin build apk --release --split-per-abi 10 | $flutterBin build apk --release 11 | -------------------------------------------------------------------------------- /build_release_linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | flutterBin=flutter/bin/flutter 4 | 5 | mkdir -p ./release 6 | 7 | $flutterBin clean 8 | $flutterBin build linux --target-platform linux-x64 9 | 10 | # compress to release directory 11 | tar -czf "release/enthusiast_tea_timer_linux.tar.gz" -C "build/linux/x64/release/bundle" . 12 | -------------------------------------------------------------------------------- /build_release_web.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | flutterBin=flutter/bin/flutter 4 | 5 | mkdir -p ./release 6 | $flutterBin clean 7 | 8 | $flutterBin build web --release 9 | 10 | # compress to release directory 11 | tar -czf "release/enthusiast_tea_timer_web.tar.gz" -C "build/web" . 12 | -------------------------------------------------------------------------------- /build_release_windows.bat: -------------------------------------------------------------------------------- 1 | mkdir release 2 | 3 | call .\flutter\bin\flutter.bat clean 4 | call .\flutter\bin\flutter.bat build windows 5 | 6 | :: zip to release directory 7 | powershell Compress-Archive -Force "build/windows/x64/runner/Release/*" "release/enthusiast_tea_timer_windows.zip" 8 | 9 | pause -------------------------------------------------------------------------------- /ci_files/keystore-github-ci.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ci_files/keystore-github-ci.jks -------------------------------------------------------------------------------- /flutter_launcher_icons.yaml: -------------------------------------------------------------------------------- 1 | flutter_launcher_icons: 2 | android: true 3 | ios: false 4 | windows: 5 | generate: true 6 | image_path: "assets/icon_simple_colored_512.png" 7 | icon_size: 48 8 | image_path: "assets/icon_simple_colored_512.png" 9 | adaptive_icon_foreground: "assets/icon_simple_512_extra_padding.png" 10 | adaptive_icon_background: "#009688" -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 13.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 44 | 50 | 51 | 52 | 53 | 54 | 66 | 68 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Infusion Timer 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | infusion_timer 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/additional_license_factory.dart: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import 'package:flutter/foundation.dart'; 4 | 5 | class AdditionalLicenseFactory { 6 | static Stream create() async* { 7 | yield AdditionalLicense( 8 | ['hand-bell-ringing-sound'], 9 | [ 10 | const LicenseParagraph(""""Free Sounds Library" 11 | 12 | 13 | Free Sound Effects Site. 14 | 15 | 16 | Licence: License: Attribution 4.0 International (CC BY 4.0). You are allowed to use sound effects free of charge and royalty free in your multimedia projects for commercial or non-commercial purposes. 17 | 18 | 19 | http://www.freesoundslibrary.com 20 | 21 | https://www.freesoundslibrary.com/hand-bell-ringing-sound/ 22 | BY SPANAC 23 | 24 | converted to wav by Sesu8642 using ffmpeg""", 0), 25 | ], 26 | ); 27 | } 28 | } 29 | 30 | class AdditionalLicense extends LicenseEntry { 31 | @override 32 | final Iterable packages; 33 | 34 | @override 35 | final Iterable paragraphs; 36 | 37 | AdditionalLicense(this.packages, this.paragraphs); 38 | } 39 | -------------------------------------------------------------------------------- /lib/backup_data.dart: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import 'package:infusion_timer/tea.dart'; 4 | 5 | const int _dataSchemaVersion = 1; 6 | 7 | class BackupData { 8 | final int? _teaVesselSizeMlPref; 9 | final List? _teas; 10 | final Map? _savedSessions; 11 | 12 | BackupData(this._teaVesselSizeMlPref, this._teas, this._savedSessions); 13 | 14 | BackupData.fromJson(Map json) 15 | : _teaVesselSizeMlPref = json['teaVesselSizeMlPref'], 16 | _teas = List.from(json['teas'].map((i) => Tea.fromJson(i))), 17 | _savedSessions = Map.from( 18 | json['savedSessions'].map( 19 | (key, val) => MapEntry(double.parse(key), val), 20 | ), 21 | ); 22 | 23 | Map toJson() => { 24 | 'version': _dataSchemaVersion, 25 | 'teaVesselSizeMlPref': _teaVesselSizeMlPref, 26 | 'teas': _teas, 27 | 'savedSessions': _savedSessions!.map( 28 | (key, value) => MapEntry(key.toString(), value), 29 | ), 30 | }; 31 | 32 | validate() { 33 | if (_teaVesselSizeMlPref == null) { 34 | throw const FormatException("teaVesselSizeMlPref is required."); 35 | } 36 | if (_teas == null) { 37 | throw const FormatException("teas is required."); 38 | } 39 | if (_savedSessions == null) { 40 | throw const FormatException("savedSessions is required."); 41 | } 42 | for (var tea in _teas) { 43 | tea.validate(); 44 | } 45 | if (_teas.map((tea) => tea.id).toSet().length < _teas.length) { 46 | throw const FormatException("Tea IDs are not unique."); 47 | } 48 | _savedSessions.forEach((key, value) { 49 | if (value <= 1) { 50 | throw const FormatException( 51 | "savedSessions contains a too small infusion index.", 52 | ); 53 | } 54 | try { 55 | Tea matchingTea = _teas.firstWhere((tea) => tea.id == key); 56 | if (value > matchingTea.infusions.length) { 57 | throw const FormatException( 58 | "savedSessions contains a session that is too large.", 59 | ); 60 | } 61 | } on StateError { 62 | throw const FormatException( 63 | "savedSessions contains a session for a nonexistent tea.", 64 | ); 65 | } 66 | }); 67 | } 68 | 69 | int get teaVesselSizeMlPref { 70 | return _teaVesselSizeMlPref!; 71 | } 72 | 73 | List get teas { 74 | return _teas!; 75 | } 76 | 77 | Map get savedSessions { 78 | return _savedSessions!; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/id_generator.dart: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import 'dart:math'; 4 | 5 | class IdGenerator { 6 | static final Random _random = Random(); 7 | 8 | static double nextdouble() { 9 | // good enough for this 10 | return _random.nextDouble(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import 'dart:io'; 4 | 5 | import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; 6 | import 'package:flutter/foundation.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 9 | import 'package:infusion_timer/additional_license_factory.dart'; 10 | import 'package:infusion_timer/persistence_service.dart'; 11 | import 'package:infusion_timer/widgets/collection_page.dart'; 12 | 13 | void main() async { 14 | WidgetsFlutterBinding.ensureInitialized(); 15 | if (!kIsWeb && Platform.isAndroid) { 16 | await AndroidAlarmManager.initialize(); 17 | } 18 | 19 | const String notificationIconName = 'notification_icon'; 20 | 21 | // initialize FlutterLocalNotificationsPlugin 22 | FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = 23 | FlutterLocalNotificationsPlugin(); 24 | const AndroidInitializationSettings initializationSettingsAndroid = 25 | AndroidInitializationSettings(notificationIconName); 26 | const LinuxInitializationSettings initializationSettingsLinux = 27 | LinuxInitializationSettings(defaultActionName: 'OK'); 28 | const InitializationSettings initializationSettings = InitializationSettings( 29 | android: initializationSettingsAndroid, 30 | linux: initializationSettingsLinux, 31 | ); 32 | await flutterLocalNotificationsPlugin.initialize(initializationSettings); 33 | 34 | // register additional licenses 35 | LicenseRegistry.addLicense(AdditionalLicenseFactory.create); 36 | 37 | // init persistence service 38 | await PersistenceService.init(); 39 | 40 | runApp(const InfusionTimer()); 41 | } 42 | 43 | class InfusionTimer extends StatefulWidget { 44 | const InfusionTimer({super.key}); 45 | 46 | @override 47 | AppState createState() { 48 | return AppState(); 49 | } 50 | } 51 | 52 | class AppState extends State { 53 | @override 54 | Widget build(BuildContext context) { 55 | return MaterialApp( 56 | title: 'Enthusiast Tea Timer', 57 | theme: ThemeData( 58 | colorScheme: const ColorScheme.light( 59 | primary: Colors.teal, 60 | secondary: Colors.teal, 61 | tertiary: Colors.teal, 62 | ), 63 | useMaterial3: true, 64 | ), 65 | home: const CollectionPage(), 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/persistence_service.dart: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import 'dart:convert'; 4 | 5 | import 'package:flutter/services.dart' show rootBundle; 6 | import 'package:infusion_timer/backup_data.dart'; 7 | import 'package:infusion_timer/tea.dart'; 8 | import 'package:shared_preferences/shared_preferences.dart'; 9 | 10 | const String _teaVesselSizeSaveKey = "tea_vessel_size"; 11 | const String _teasSaveKey = "teas"; 12 | const String _sessionSavePrefix = "session:"; 13 | 14 | class PersistenceService { 15 | static late SharedPreferences _prefs; 16 | static int _teaVesselSizeMlPref = 100; 17 | static List _teas = []; 18 | static Map _savedSessions = {}; 19 | 20 | static Future init() async { 21 | // tried implementing this class as a singleton to init in the constructor but did not work because the constructor cannot be async 22 | _prefs = await SharedPreferences.getInstance(); 23 | 24 | // read preferences 25 | var savedTeaVesselSizeMlPref = _prefs.getInt(_teaVesselSizeSaveKey); 26 | if (savedTeaVesselSizeMlPref != null) { 27 | _teaVesselSizeMlPref = savedTeaVesselSizeMlPref; 28 | } 29 | 30 | // read teas 31 | var savedTeasJson = _prefs.getStringList(_teasSaveKey); 32 | if (savedTeasJson == null) { 33 | // if there in no saved data, load the default included teas 34 | var defaultTeasJson = (await rootBundle.loadString( 35 | 'assets/default_data.json', 36 | )); 37 | var teasJson = json.decode(defaultTeasJson) as List; 38 | _teas = teasJson.map((jsonTea) => Tea.fromJson(jsonTea)).toList(); 39 | } else { 40 | // if there in no saved data, load the saved teas 41 | _teas = savedTeasJson 42 | .map((teaJson) => Tea.fromJson(jsonDecode(teaJson))) 43 | .toList(); 44 | } 45 | 46 | // read sessions 47 | for (var tea in _teas) { 48 | var teaSession = _prefs.getInt(_sessionSavePrefix + tea.id.toString()); 49 | if (teaSession != null) { 50 | _savedSessions[tea.id] = teaSession; 51 | } else { 52 | _savedSessions.remove(tea.id); 53 | } 54 | } 55 | } 56 | 57 | static int get teaVesselSizeMlPref { 58 | return _teaVesselSizeMlPref; 59 | } 60 | 61 | // cannot use regular setter because this must be async 62 | static Future setTeaVesselSizeMlPref(int teaVesselSizeMl) async { 63 | _teaVesselSizeMlPref = teaVesselSizeMl; 64 | await _prefs.setInt(_teaVesselSizeSaveKey, teaVesselSizeMl); 65 | } 66 | 67 | static List get teas { 68 | return _teas; 69 | } 70 | 71 | // cannot use regular setter because this must be async 72 | static setTeas(List teas) async { 73 | _teas = teas; 74 | await _saveTeas(); 75 | } 76 | 77 | static Future _saveTeas() async { 78 | await _prefs.setStringList( 79 | _teasSaveKey, 80 | _teas.map((tea) => jsonEncode(tea)).toList(), 81 | ); 82 | } 83 | 84 | static Future addTea(Tea tea) async { 85 | _teas.insert(0, tea); 86 | await _saveTeas(); 87 | } 88 | 89 | static Future deleteTea(Tea tea) async { 90 | _teas.remove(tea); 91 | await _saveTeas(); 92 | // delete active session if any 93 | deleteSession(tea); 94 | } 95 | 96 | static Future updateTea(Tea tea) async { 97 | // tea was changed already and the change needs to be handled 98 | await _saveTeas(); 99 | // make sure the saved infusion is not bigger than the number of infusions the tea has now 100 | var savedInfusion = _prefs.getInt(_sessionSavePrefix + tea.id.toString()); 101 | if (savedInfusion != null && savedInfusion >= tea.infusions.length) { 102 | deleteSession(tea); 103 | } 104 | } 105 | 106 | static Future bringTeaToFirstPosition(Tea tea) async { 107 | _teas.remove(tea); 108 | _teas.insert(0, tea); 109 | await _saveTeas(); 110 | } 111 | 112 | // cannot use regular setter because this must be async 113 | static setSavedSessions(Map savedSessions) async { 114 | _savedSessions = savedSessions; 115 | savedSessions.forEach((key, value) async { 116 | await _prefs.setInt(_sessionSavePrefix + key.toString(), value); 117 | }); 118 | } 119 | 120 | static Map get savedSessions { 121 | return _savedSessions; 122 | } 123 | 124 | static Future getSession(Tea tea) async { 125 | return _prefs.getInt(_sessionSavePrefix + tea.id.toString()); 126 | } 127 | 128 | static Future saveSession(Tea tea, int infusion) async { 129 | _savedSessions[tea.id] = infusion; 130 | await _prefs.setInt(_sessionSavePrefix + tea.id.toString(), infusion); 131 | } 132 | 133 | static Future deleteSession(Tea tea) async { 134 | _savedSessions.remove(tea.id); 135 | await _prefs.remove(_sessionSavePrefix + tea.id.toString()); 136 | } 137 | 138 | static BackupData getBackupData() { 139 | return BackupData(_teaVesselSizeMlPref, _teas, _savedSessions); 140 | } 141 | 142 | static Future restoreFomBackup(BackupData backup) async { 143 | backup.validate(); 144 | await _prefs.clear(); 145 | await setTeaVesselSizeMlPref(backup.teaVesselSizeMlPref); 146 | await setTeas(backup.teas); 147 | await setSavedSessions(backup.savedSessions); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /lib/tea.dart: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | import 'package:infusion_timer/id_generator.dart'; 3 | 4 | class Tea { 5 | double id; 6 | String? name; 7 | int? rating; 8 | int? temperature; 9 | double? gPer100Ml; 10 | List infusions; 11 | String? subtitle; 12 | String? detailedNotes; 13 | 14 | Tea( 15 | this.id, 16 | this.name, 17 | this.temperature, 18 | this.gPer100Ml, 19 | this.infusions, 20 | this.subtitle, 21 | this.detailedNotes, 22 | this.rating, 23 | ); 24 | 25 | Tea.withGeneratedId( 26 | this.name, 27 | this.temperature, 28 | this.gPer100Ml, 29 | this.infusions, 30 | this.subtitle, 31 | this.rating, 32 | ) : id = IdGenerator.nextdouble(); 33 | 34 | Tea.copyWithGeneratedId(Tea original) 35 | : id = IdGenerator.nextdouble(), 36 | name = original.name, 37 | temperature = original.temperature, 38 | gPer100Ml = original.gPer100Ml, 39 | infusions = List.from(original.infusions), 40 | subtitle = original.subtitle, 41 | rating = original.rating; 42 | 43 | Tea.fromJson(Map json) 44 | : id = json.containsKey('id') ? json['id'] : IdGenerator.nextdouble(), 45 | name = json['name'], 46 | rating = json['rating'], 47 | temperature = json['temperature'], 48 | gPer100Ml = json['gPer100Ml'] is double 49 | ? json['gPer100Ml'] 50 | : double.parse(json['gPer100Ml'].toString()), 51 | infusions = List.from( 52 | json['infusions']?.map((i) => Infusion.fromJson(i)) ?? List.empty(), 53 | ), 54 | // subtitle used to be called notes 55 | subtitle = json['notes'] ?? json['subtitle'], 56 | detailedNotes = json['detailedNotes']; 57 | 58 | Map toJson() => { 59 | 'id': id, 60 | 'name': name, 61 | 'rating': rating, 62 | 'temperature': temperature, 63 | 'gPer100Ml': gPer100Ml, 64 | 'infusions': infusions, 65 | 'subtitle': subtitle, 66 | 'detailedNotes': detailedNotes, 67 | }; 68 | 69 | String toSharableString() => 70 | 'Name: $name' 71 | '${subtitle != null && subtitle!.isNotEmpty ? '\nSubtitle: $subtitle' : ''}' 72 | '${rating != null ? '\nRating: $rating/5' : ''}' 73 | '\nTemperature: $temperature°C' 74 | '\nAmount: $gPer100Ml g/100 ml' 75 | '\nInfusions: ${infusions.map((infusion) => "${infusion.duration}" 76 | " s").join(", ")}' 77 | '${detailedNotes != null && detailedNotes!.isNotEmpty ? '\nNotes: $detailedNotes' : ''}' 78 | '\n\nShared from Enthusiast Tea Timer.'; 79 | 80 | validate() { 81 | if (name == null) { 82 | throw const FormatException("Tea has no name."); 83 | } 84 | if (rating != null && (rating! < 0 || rating! > 5)) { 85 | throw FormatException("Tea '$name' has an invalid rating."); 86 | } 87 | if (temperature == null) { 88 | throw FormatException("Tea '$name' has no brewing temperature."); 89 | } 90 | if (gPer100Ml == null) { 91 | throw FormatException("Tea '$name' has no tea amount."); 92 | } 93 | if (infusions.isEmpty) { 94 | throw FormatException("Tea '$name' has no infusions."); 95 | } 96 | for (var infusion in infusions) { 97 | if (infusion.duration.isNegative) { 98 | throw FormatException( 99 | "Tea '$name' has an infusion with invalid duration.", 100 | ); 101 | } 102 | } 103 | if (subtitle == null) { 104 | throw FormatException("Tea '$name' has no subtitle."); 105 | } 106 | } 107 | } 108 | 109 | class Infusion { 110 | int duration; 111 | 112 | Infusion(this.duration); 113 | 114 | Infusion.fromJson(Map json) : duration = json['duration']; 115 | 116 | Map toJson() => {'duration': duration}; 117 | } 118 | -------------------------------------------------------------------------------- /lib/widgets/confirm_dialog.dart: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class ConfirmDialog extends StatelessWidget { 6 | final String title; 7 | final String confirmText; 8 | final Function() okCallback; 9 | final Function() cancelCallback; 10 | 11 | const ConfirmDialog( 12 | this.title, 13 | this.confirmText, 14 | this.okCallback, 15 | this.cancelCallback, { 16 | super.key, 17 | }); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return AlertDialog( 22 | title: Text(title), 23 | content: SingleChildScrollView( 24 | child: ListBody(children: [Text(confirmText)]), 25 | ), 26 | actions: [ 27 | TextButton( 28 | onPressed: () => cancelCallback(), 29 | child: const Text('Cancel'), 30 | ), 31 | TextButton(onPressed: () => okCallback(), child: const Text('OK')), 32 | ], 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/widgets/data_backup_dialog.dart: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import 'dart:convert'; 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/services.dart'; 7 | import 'package:infusion_timer/persistence_service.dart'; 8 | 9 | class DataBackupDialog extends StatelessWidget { 10 | const DataBackupDialog({super.key}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return AlertDialog( 15 | title: const Text('Copy this text and store it somewhere safe.'), 16 | content: SingleChildScrollView( 17 | child: Column( 18 | crossAxisAlignment: CrossAxisAlignment.start, 19 | children: [ 20 | TextFormField( 21 | initialValue: jsonEncode(PersistenceService.getBackupData()), 22 | keyboardType: TextInputType.multiline, 23 | maxLines: 5, 24 | readOnly: true, 25 | ), 26 | Padding( 27 | padding: const EdgeInsets.only(top: 10), 28 | child: TextButton( 29 | onPressed: () { 30 | Clipboard.setData( 31 | ClipboardData( 32 | text: jsonEncode(PersistenceService.getBackupData()), 33 | ), 34 | ); 35 | ScaffoldMessenger.of(context).showSnackBar( 36 | const SnackBar(content: Text("Copied to clipboard.")), 37 | ); 38 | }, 39 | child: const Text('Copy to clipboard'), 40 | ), 41 | ), 42 | ], 43 | ), 44 | ), 45 | actions: [ 46 | TextButton( 47 | onPressed: () => Navigator.of(context).pop(), 48 | child: const Text('OK'), 49 | ), 50 | ], 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/widgets/data_restore_dialog.dart: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import 'dart:convert'; 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/services.dart'; 7 | import 'package:infusion_timer/backup_data.dart'; 8 | import 'package:infusion_timer/persistence_service.dart'; 9 | import 'package:infusion_timer/widgets/confirm_dialog.dart'; 10 | 11 | class DataRestoreDialog extends StatelessWidget { 12 | final TextEditingController textController = TextEditingController(); 13 | 14 | DataRestoreDialog({super.key}); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return AlertDialog( 19 | title: const Text('Paste the backed up text here.'), 20 | content: SingleChildScrollView( 21 | child: Column( 22 | crossAxisAlignment: CrossAxisAlignment.start, 23 | children: [ 24 | TextFormField( 25 | controller: textController, 26 | keyboardType: TextInputType.multiline, 27 | maxLines: 5, 28 | ), 29 | Padding( 30 | padding: const EdgeInsets.only(top: 10), 31 | child: TextButton( 32 | onPressed: () async => textController.text = 33 | (await Clipboard.getData("text/plain"))?.text ?? "", 34 | child: const Text('Paste from clipboard'), 35 | ), 36 | ), 37 | ], 38 | ), 39 | ), 40 | actions: [ 41 | TextButton( 42 | onPressed: () => Navigator.of(context).pop(), 43 | child: const Text('Cancel'), 44 | ), 45 | TextButton( 46 | onPressed: () => showDialog( 47 | context: context, 48 | barrierDismissible: false, 49 | builder: (BuildContext context) => ConfirmDialog( 50 | "Are you sure?", 51 | "Your current tea collection will be lost and replaced by the backup.", 52 | () { 53 | Navigator.of(context).pop(); 54 | try { 55 | var backup = BackupData.fromJson( 56 | jsonDecode(textController.text), 57 | ); 58 | PersistenceService.restoreFomBackup(backup) 59 | .then((value) { 60 | if (context.mounted) { 61 | return Navigator.of(context).pop(); 62 | } 63 | }) 64 | .onError((e, stackTrace) { 65 | if (context.mounted) { 66 | ScaffoldMessenger.of( 67 | context, 68 | ).showSnackBar(SnackBar(content: Text(e.toString()))); 69 | } 70 | }); 71 | } catch (e) { 72 | ScaffoldMessenger.of( 73 | context, 74 | ).showSnackBar(SnackBar(content: Text(e.toString()))); 75 | } 76 | }, 77 | () => Navigator.of(context).pop(), 78 | ), 79 | ), 80 | child: const Text('Restore'), 81 | ), 82 | ], 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/widgets/notes_page.dart: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import 'dart:async'; 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:infusion_timer/persistence_service.dart'; 7 | import 'package:infusion_timer/tea.dart'; 8 | 9 | class NotesPage extends StatefulWidget { 10 | final Tea tea; 11 | 12 | const NotesPage({super.key, required this.tea}); 13 | 14 | @override 15 | NotesPageState createState() => NotesPageState(); 16 | } 17 | 18 | class NotesPageState extends State { 19 | final _formKey = GlobalKey(); 20 | bool _hasUnsavedChanges = false; 21 | 22 | Future _onPopInvoked(bool didPop, dynamic result) async { 23 | // confirm cancelling infusion if going back to collection page 24 | if (didPop) { 25 | return; 26 | } 27 | final int? action = await showDialog( 28 | context: context, 29 | builder: (context) { 30 | return AlertDialog( 31 | title: const Text('Save unsaved changes?'), 32 | content: const Text('You have unsaved changes.'), 33 | actions: [ 34 | TextButton( 35 | onPressed: () { 36 | Navigator.pop(context, 0); 37 | }, 38 | child: const Text('Save and Close'), 39 | ), 40 | TextButton( 41 | onPressed: () => Navigator.pop(context, 1), 42 | child: const Text('Discard and Close'), 43 | ), 44 | TextButton( 45 | onPressed: () => Navigator.pop(context, 2), 46 | child: const Text('Cancel'), 47 | ), 48 | ], 49 | ); 50 | }, 51 | ); 52 | switch (action) { 53 | case 0: 54 | _formKey.currentState!.save(); 55 | PersistenceService.updateTea(widget.tea); 56 | if (mounted) { 57 | Navigator.pop(context); 58 | } 59 | break; 60 | case 1: 61 | if (mounted) { 62 | Navigator.pop(context); 63 | } 64 | break; 65 | case 2: 66 | // nothing to do 67 | break; 68 | } 69 | } 70 | 71 | @override 72 | Widget build(BuildContext context) { 73 | return PopScope( 74 | canPop: !_hasUnsavedChanges, 75 | onPopInvokedWithResult: _onPopInvoked, 76 | child: Scaffold( 77 | appBar: AppBar(title: const Text("Notes")), 78 | body: Form( 79 | key: _formKey, 80 | child: Column( 81 | children: [ 82 | Expanded( 83 | child: Card( 84 | child: Column( 85 | children: [ 86 | ListTile(title: Center(child: Text(widget.tea.name!))), 87 | Expanded( 88 | child: TextFormField( 89 | minLines: 15, 90 | initialValue: widget.tea.detailedNotes, 91 | maxLines: null, 92 | // using collapsed to hide black line on the bottom 93 | decoration: const InputDecoration.collapsed( 94 | hintText: 'Enter your notes here', 95 | ), 96 | onChanged: (value) { 97 | setState(() => _hasUnsavedChanges = true); 98 | }, 99 | onSaved: (value) { 100 | widget.tea.detailedNotes = value; 101 | }, 102 | ), 103 | ), 104 | const SizedBox(height: 10), 105 | FilledButton( 106 | onPressed: () { 107 | _formKey.currentState!.save(); 108 | PersistenceService.updateTea(widget.tea); 109 | Navigator.pop(context); 110 | }, 111 | child: const Text( 112 | 'Save and Close', 113 | style: TextStyle(fontWeight: FontWeight.bold), 114 | ), 115 | ), 116 | SizedBox( 117 | height: MediaQuery.of( 118 | context, 119 | ).systemGestureInsets.bottom, 120 | ), 121 | ], 122 | ), 123 | ), 124 | ), 125 | ], 126 | ), 127 | ), 128 | ), 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/widgets/preferences_page.dart: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:infusion_timer/persistence_service.dart'; 6 | import 'package:infusion_timer/widgets/data_backup_dialog.dart'; 7 | import 'package:infusion_timer/widgets/data_restore_dialog.dart'; 8 | 9 | class PreferencesPage extends StatefulWidget { 10 | const PreferencesPage({super.key}); 11 | 12 | @override 13 | PreferencesPageState createState() => PreferencesPageState(); 14 | } 15 | 16 | class PreferencesPageState extends State { 17 | final _formKey = GlobalKey(); 18 | final TextEditingController _vesselSizeController = TextEditingController(); 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | setState(() { 24 | _vesselSizeController.value = TextEditingValue( 25 | text: PersistenceService.teaVesselSizeMlPref.toString(), 26 | ); 27 | }); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return Scaffold( 33 | appBar: AppBar(title: const Text("Preferences")), 34 | body: Form( 35 | key: _formKey, 36 | child: SingleChildScrollView( 37 | child: Padding( 38 | padding: const EdgeInsets.all(10.0), 39 | child: Column( 40 | crossAxisAlignment: CrossAxisAlignment.start, 41 | children: [ 42 | const Text( 43 | "Size of your tea brewing vessel in ml", 44 | style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), 45 | ), 46 | TextFormField( 47 | controller: _vesselSizeController, 48 | decoration: const InputDecoration( 49 | icon: Icon(Icons.free_breakfast), 50 | ), 51 | keyboardType: TextInputType.number, 52 | maxLength: 5, 53 | inputFormatters: [FilteringTextInputFormatter.digitsOnly], 54 | validator: (value) { 55 | if (value == null || value.isEmpty) { 56 | return "Enter a value"; 57 | } else { 58 | return null; 59 | } 60 | }, 61 | onChanged: (value) async { 62 | if (_formKey.currentState!.validate()) { 63 | await PersistenceService.setTeaVesselSizeMlPref( 64 | int.parse(value), 65 | ); 66 | } 67 | }, 68 | ), 69 | const Text( 70 | "Data backup/restore", 71 | style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), 72 | ), 73 | Padding( 74 | padding: const EdgeInsets.symmetric(vertical: 10), 75 | child: ElevatedButton( 76 | onPressed: () { 77 | showDialog( 78 | context: context, 79 | barrierDismissible: false, 80 | builder: (BuildContext context) => 81 | const DataBackupDialog(), 82 | ); 83 | }, 84 | child: const Text("Data Backup"), 85 | ), 86 | ), 87 | ElevatedButton( 88 | onPressed: () async { 89 | await showDialog( 90 | context: context, 91 | barrierDismissible: false, 92 | builder: (BuildContext context) => DataRestoreDialog(), 93 | ); 94 | setState(() { 95 | _vesselSizeController.value = TextEditingValue( 96 | text: PersistenceService.teaVesselSizeMlPref.toString(), 97 | ); 98 | }); 99 | }, 100 | child: const Text("Data Restore"), 101 | ), 102 | ], 103 | ), 104 | ), 105 | ), 106 | ), 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/widgets/star_rating_form_field.dart: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class StarRatingFormField extends FormField { 6 | StarRatingFormField({ 7 | super.key, 8 | super.onSaved, 9 | super.validator, 10 | int super.initialValue = 0, 11 | AutovalidateMode autovalidate = AutovalidateMode.disabled, 12 | }) : super( 13 | autovalidateMode: autovalidate, 14 | builder: (FormFieldState state) { 15 | return Row( 16 | children: [ 17 | for (int i = 1; i <= 5; i++) 18 | InkWell( 19 | onTap: () => state.didChange(i), 20 | borderRadius: BorderRadius.circular(10), 21 | child: Icon( 22 | state.value! >= i 23 | ? Icons.star_rounded 24 | : Icons.star_outline_rounded, 25 | ), 26 | ), 27 | ], 28 | ); 29 | }, 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /lib/widgets/tea_actions_bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:infusion_timer/tea.dart'; 5 | import 'package:infusion_timer/widgets/confirm_dialog.dart'; 6 | 7 | class TeaActionsBottomSheet extends StatelessWidget { 8 | final Tea tea; 9 | final Function(Tea) shareCallback; 10 | final Function(Tea) editCallback; 11 | final Function(Tea) copyCallback; 12 | final Function(Tea) deleteCallback; 13 | 14 | const TeaActionsBottomSheet( 15 | this.tea, 16 | this.shareCallback, 17 | this.editCallback, 18 | this.copyCallback, 19 | this.deleteCallback, { 20 | super.key, 21 | }); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Wrap( 26 | children: [ 27 | ListTile( 28 | leading: const Icon(Icons.share), 29 | title: const Text("Share"), 30 | onTap: () => shareCallback(tea), 31 | ), 32 | ListTile( 33 | leading: const Icon(Icons.edit), 34 | title: const Text("Edit"), 35 | onTap: () => editCallback(tea), 36 | ), 37 | ListTile( 38 | leading: const Icon(Icons.copy), 39 | title: const Text("Copy"), 40 | onTap: () => copyCallback(tea), 41 | ), 42 | ListTile( 43 | leading: const Icon(Icons.delete), 44 | title: const Text("Delete"), 45 | onTap: () { 46 | showDialog( 47 | context: context, 48 | builder: (BuildContext context) { 49 | return ConfirmDialog( 50 | "Are you sure?", 51 | '"${tea.name}" will be deleted permanently.', 52 | () { 53 | Navigator.of(context).pop(); 54 | Navigator.of(context).pop(); 55 | deleteCallback(tea); 56 | }, 57 | () { 58 | Navigator.of(context).pop(); 59 | Navigator.of(context).pop(); 60 | }, 61 | ); 62 | }, 63 | ); 64 | }, 65 | ), 66 | ], 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/widgets/tea_card.dart: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:infusion_timer/tea.dart'; 5 | 6 | class TeaCard extends StatelessWidget { 7 | final Tea tea; 8 | final Function(Tea)? tapCallback; 9 | final Function(Tea)? longPressCallback; 10 | final int teaVesselSize; 11 | final int? infusionOfActiveSession; 12 | 13 | const TeaCard( 14 | this.tea, 15 | this.tapCallback, 16 | this.longPressCallback, 17 | this.teaVesselSize, 18 | this.infusionOfActiveSession, { 19 | super.key, 20 | }); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return Center( 25 | child: Card( 26 | surfaceTintColor: Theme.of(context).colorScheme.secondary, 27 | child: GestureDetector( 28 | child: InkWell( 29 | onTap: tapCallback == null ? null : () => tapCallback!(tea), 30 | onLongPress: longPressCallback == null 31 | ? null 32 | : () => longPressCallback!(tea), 33 | child: Column( 34 | mainAxisSize: MainAxisSize.min, 35 | children: [ 36 | ListTile( 37 | title: Center(child: Text(tea.name!)), 38 | subtitle: tea.subtitle!.isEmpty 39 | ? null 40 | : Center(child: Text(tea.subtitle!)), 41 | ), 42 | Wrap( 43 | alignment: WrapAlignment.center, 44 | children: [ 45 | Row( 46 | mainAxisSize: MainAxisSize.min, 47 | children: [ 48 | const Icon(Icons.thermostat_outlined), 49 | Text("${tea.temperature} °C"), 50 | const SizedBox(width: 8), 51 | ], 52 | ), 53 | Row( 54 | mainAxisSize: MainAxisSize.min, 55 | children: [ 56 | const Icon(Icons.grass), 57 | Text( 58 | "${((tea.gPer100Ml! * teaVesselSize).round() / 100).toString()} g/ ${teaVesselSize.toString()}ml", 59 | ), 60 | const SizedBox(width: 8), 61 | ], 62 | ), 63 | Row( 64 | mainAxisSize: MainAxisSize.min, 65 | children: [ 66 | const Icon(Icons.repeat), 67 | Text("${tea.infusions.length} infusions"), 68 | const SizedBox(width: 8), 69 | ], 70 | ), 71 | Row( 72 | mainAxisSize: MainAxisSize.min, 73 | children: [ 74 | const Icon(Icons.star_outline_rounded), 75 | Text("${tea.rating ?? '-'}/5"), 76 | ], 77 | ), 78 | ], 79 | ), 80 | Row( 81 | mainAxisAlignment: MainAxisAlignment.center, 82 | children: infusionOfActiveSession == null 83 | ? [] 84 | : [ 85 | Expanded( 86 | child: Text( 87 | textAlign: TextAlign.center, 88 | "Current brew: ${tea.infusions.length - infusionOfActiveSession! + 1} more infusion(s) remaining.", 89 | ), 90 | ), 91 | ], 92 | ), 93 | ], 94 | ), 95 | ), 96 | ), 97 | ), 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/widgets/tea_input_dialog.dart: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:infusion_timer/tea.dart'; 6 | import 'package:infusion_timer/widgets/star_rating_form_field.dart'; 7 | 8 | class TeaInputDialog extends StatefulWidget { 9 | final Tea tea; 10 | final Function(Tea) saveCallback; 11 | final Function(Tea) cancelCallback; 12 | 13 | const TeaInputDialog( 14 | this.tea, 15 | this.saveCallback, 16 | this.cancelCallback, { 17 | super.key, 18 | }); 19 | 20 | @override 21 | TeaInputFormFormState createState() { 22 | return TeaInputFormFormState(); 23 | } 24 | } 25 | 26 | class TeaInputFormFormState extends State { 27 | final _formKey = GlobalKey(); 28 | 29 | TextEditingController newInfusionController = TextEditingController(); 30 | 31 | _addInfusion() { 32 | setState(() { 33 | int? parsedInt = int.tryParse(newInfusionController.text); 34 | if (parsedInt != null) { 35 | widget.tea.infusions.add(Infusion(parsedInt)); 36 | } 37 | }); 38 | newInfusionController.clear(); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return AlertDialog( 44 | title: const Text('Edit Tea'), 45 | content: Form( 46 | key: _formKey, 47 | child: SingleChildScrollView( 48 | child: Column( 49 | crossAxisAlignment: CrossAxisAlignment.start, 50 | children: [ 51 | Row( 52 | children: [ 53 | const Text("Rating "), 54 | StarRatingFormField( 55 | initialValue: widget.tea.rating ?? 0, 56 | onSaved: (value) { 57 | widget.tea.rating = value == 0 ? null : value; 58 | }, 59 | ), 60 | ], 61 | ), 62 | TextFormField( 63 | initialValue: widget.tea.name, 64 | decoration: const InputDecoration(hintText: 'Name'), 65 | validator: (value) { 66 | if (value == null || value.isEmpty) { 67 | return "Enter a name"; 68 | } else { 69 | return null; 70 | } 71 | }, 72 | onSaved: (value) { 73 | widget.tea.name = value!; 74 | }, 75 | ), 76 | TextFormField( 77 | initialValue: widget.tea.temperature == null 78 | ? "" 79 | : widget.tea.temperature.toString(), 80 | keyboardType: TextInputType.number, 81 | inputFormatters: [FilteringTextInputFormatter.digitsOnly], 82 | validator: (value) { 83 | if (value == null || value.isEmpty) { 84 | return "Enter the brewing temperature"; 85 | } else { 86 | return null; 87 | } 88 | }, 89 | onSaved: (value) { 90 | widget.tea.temperature = int.parse(value!); 91 | }, 92 | decoration: const InputDecoration( 93 | hintText: 'Brewing Temperature in °C', 94 | ), 95 | ), 96 | TextFormField( 97 | initialValue: widget.tea.gPer100Ml == null 98 | ? "" 99 | : widget.tea.gPer100Ml.toString(), 100 | keyboardType: TextInputType.number, 101 | inputFormatters: [ 102 | FilteringTextInputFormatter.allow(RegExp('[0-9,.]')), 103 | ], 104 | validator: (value) { 105 | num? parsed = num.tryParse(value?.replaceAll(',', '.') ?? ''); 106 | if (parsed == null) { 107 | return "Invalid Amount"; 108 | } else { 109 | return null; 110 | } 111 | }, 112 | decoration: const InputDecoration( 113 | hintText: 'Amount in g/100ml', 114 | ), 115 | onSaved: (value) { 116 | widget.tea.gPer100Ml = double.parse( 117 | value?.replaceAll(',', '.') ?? '', 118 | ); 119 | }, 120 | ), 121 | TextFormField( 122 | initialValue: widget.tea.subtitle, 123 | keyboardType: TextInputType.multiline, 124 | maxLines: null, 125 | decoration: const InputDecoration(hintText: 'Subtitle'), 126 | onSaved: (value) { 127 | widget.tea.subtitle = value ?? ""; 128 | }, 129 | ), 130 | Text( 131 | "\nInfusions", 132 | style: Theme.of(context).textTheme.titleLarge, 133 | ), 134 | ...widget.tea.infusions.map( 135 | (infusion) => ListTile( 136 | trailing: IconButton( 137 | icon: const Icon(Icons.delete), 138 | splashRadius: Material.defaultSplashRadius / 1.5, 139 | tooltip: 'delete', 140 | onPressed: () { 141 | setState(() { 142 | widget.tea.infusions.removeAt( 143 | widget.tea.infusions.indexOf(infusion), 144 | ); 145 | }); 146 | }, 147 | ), 148 | title: Text( 149 | "${widget.tea.infusions.indexOf(infusion) + 1}. ${infusion.duration}s", 150 | ), 151 | contentPadding: const EdgeInsets.all(0), 152 | ), 153 | ), 154 | TextFormField( 155 | controller: newInfusionController, 156 | keyboardType: TextInputType.number, 157 | inputFormatters: [FilteringTextInputFormatter.digitsOnly], 158 | decoration: InputDecoration( 159 | hintText: 'Infusion time in s', 160 | suffixIcon: IconButton( 161 | splashRadius: Material.defaultSplashRadius / 1.5, 162 | icon: const Icon(Icons.add), 163 | onPressed: () { 164 | _addInfusion(); 165 | }, 166 | ), 167 | ), 168 | validator: (value) { 169 | if (widget.tea.infusions.isEmpty) { 170 | return "Add at least one infusion"; 171 | } else { 172 | return null; 173 | } 174 | }, 175 | ), 176 | ], 177 | ), 178 | ), 179 | ), 180 | actions: [ 181 | TextButton( 182 | onPressed: () { 183 | widget.cancelCallback(widget.tea); 184 | }, 185 | child: const Text('Cancel'), 186 | ), 187 | TextButton( 188 | onPressed: () { 189 | _addInfusion(); 190 | setState(() { 191 | if (_formKey.currentState!.validate()) { 192 | _formKey.currentState!.save(); 193 | widget.saveCallback(widget.tea); 194 | } 195 | }); 196 | }, 197 | child: const Text( 198 | 'Save', 199 | style: TextStyle(fontWeight: FontWeight.bold), 200 | ), 201 | ), 202 | ], 203 | ); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.13) 3 | project(runner LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "enthusiast_tea_timer") 8 | # The unique GTK application identifier for this application. See: 9 | # https://wiki.gnome.org/HowDoI/ChooseApplicationID 10 | set(APPLICATION_ID "com.sesu8642.infusion_timer") 11 | 12 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 13 | # versions of CMake. 14 | cmake_policy(SET CMP0063 NEW) 15 | 16 | # Load bundled libraries from the lib/ directory relative to the binary. 17 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 18 | 19 | # Root filesystem for cross-building. 20 | if(FLUTTER_TARGET_PLATFORM_SYSROOT) 21 | set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) 22 | set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) 23 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 24 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 25 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 26 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 27 | endif() 28 | 29 | # Define build configuration options. 30 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 31 | set(CMAKE_BUILD_TYPE "Debug" CACHE 32 | STRING "Flutter build mode" FORCE) 33 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 34 | "Debug" "Profile" "Release") 35 | endif() 36 | 37 | # Compilation settings that should be applied to most targets. 38 | # 39 | # Be cautious about adding new options here, as plugins use this function by 40 | # default. In most cases, you should add new options to specific targets instead 41 | # of modifying this function. 42 | function(APPLY_STANDARD_SETTINGS TARGET) 43 | target_compile_features(${TARGET} PUBLIC cxx_std_14) 44 | target_compile_options(${TARGET} PRIVATE -Wall -Werror) 45 | target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") 46 | target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") 47 | endfunction() 48 | 49 | # Flutter library and tool build rules. 50 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 51 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 52 | 53 | # System-level dependencies. 54 | find_package(PkgConfig REQUIRED) 55 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 56 | 57 | # Application build; see runner/CMakeLists.txt. 58 | add_subdirectory("runner") 59 | 60 | # Run the Flutter tool portions of the build. This must not be removed. 61 | add_dependencies(${BINARY_NAME} flutter_assemble) 62 | 63 | # Only the install-generated bundle's copy of the executable will launch 64 | # correctly, since the resources must in the right relative locations. To avoid 65 | # people trying to run the unbundled copy, put it in a subdirectory instead of 66 | # the default top-level location. 67 | set_target_properties(${BINARY_NAME} 68 | PROPERTIES 69 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" 70 | ) 71 | 72 | 73 | # Generated plugin build rules, which manage building the plugins and adding 74 | # them to the application. 75 | include(flutter/generated_plugins.cmake) 76 | 77 | 78 | # === Installation === 79 | # By default, "installing" just makes a relocatable bundle in the build 80 | # directory. 81 | set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") 82 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 83 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 84 | endif() 85 | 86 | # Start with a clean build bundle directory every time. 87 | install(CODE " 88 | file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") 89 | " COMPONENT Runtime) 90 | 91 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 92 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") 93 | 94 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 95 | COMPONENT Runtime) 96 | 97 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 98 | COMPONENT Runtime) 99 | 100 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 101 | COMPONENT Runtime) 102 | 103 | foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) 104 | install(FILES "${bundled_library}" 105 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 106 | COMPONENT Runtime) 107 | endforeach(bundled_library) 108 | 109 | # Copy the native assets provided by the build.dart from all packages. 110 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") 111 | install(DIRECTORY "${NATIVE_ASSETS_DIR}" 112 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 113 | COMPONENT Runtime) 114 | 115 | # Fully re-copy the assets directory on each build to avoid having stale files 116 | # from a previous install. 117 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 118 | install(CODE " 119 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 120 | " COMPONENT Runtime) 121 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 122 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 123 | 124 | # Install the AOT library on non-Debug builds only. 125 | if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") 126 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 127 | COMPONENT Runtime) 128 | endif() 129 | -------------------------------------------------------------------------------- /linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | void fl_register_plugins(FlPluginRegistry* registry) { 14 | g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = 15 | fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); 16 | audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar); 17 | g_autoptr(FlPluginRegistrar) flutter_volume_controller_registrar = 18 | fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterVolumeControllerPlugin"); 19 | flutter_volume_controller_plugin_register_with_registrar(flutter_volume_controller_registrar); 20 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 21 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 22 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 23 | } 24 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | audioplayers_linux 7 | flutter_volume_controller 8 | url_launcher_linux 9 | ) 10 | 11 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 12 | ) 13 | 14 | set(PLUGIN_BUNDLED_LIBRARIES) 15 | 16 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 17 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 18 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 19 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 20 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 21 | endforeach(plugin) 22 | 23 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 24 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 26 | endforeach(ffi_plugin) 27 | -------------------------------------------------------------------------------- /linux/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} 10 | "main.cc" 11 | "my_application.cc" 12 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 13 | ) 14 | 15 | # Apply the standard set of build settings. This can be removed for applications 16 | # that need different build settings. 17 | apply_standard_settings(${BINARY_NAME}) 18 | 19 | # Add preprocessor definitions for the application ID. 20 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") 21 | 22 | # Add dependency libraries. Add any application-specific dependencies here. 23 | target_link_libraries(${BINARY_NAME} PRIVATE flutter) 24 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 25 | 26 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 27 | -------------------------------------------------------------------------------- /linux/runner/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /linux/runner/my_application.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | #include 4 | #ifdef GDK_WINDOWING_X11 5 | #include 6 | #endif 7 | 8 | #include "flutter/generated_plugin_registrant.h" 9 | 10 | struct _MyApplication { 11 | GtkApplication parent_instance; 12 | char** dart_entrypoint_arguments; 13 | }; 14 | 15 | G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) 16 | 17 | // Called when first Flutter frame received. 18 | static void first_frame_cb(MyApplication* self, FlView *view) 19 | { 20 | gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); 21 | } 22 | 23 | // Implements GApplication::activate. 24 | static void my_application_activate(GApplication* application) { 25 | MyApplication* self = MY_APPLICATION(application); 26 | GtkWindow* window = 27 | GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); 28 | 29 | // Use a header bar when running in GNOME as this is the common style used 30 | // by applications and is the setup most users will be using (e.g. Ubuntu 31 | // desktop). 32 | // If running on X and not using GNOME then just use a traditional title bar 33 | // in case the window manager does more exotic layout, e.g. tiling. 34 | // If running on Wayland assume the header bar will work (may need changing 35 | // if future cases occur). 36 | gboolean use_header_bar = TRUE; 37 | #ifdef GDK_WINDOWING_X11 38 | GdkScreen* screen = gtk_window_get_screen(window); 39 | if (GDK_IS_X11_SCREEN(screen)) { 40 | const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); 41 | if (g_strcmp0(wm_name, "GNOME Shell") != 0) { 42 | use_header_bar = FALSE; 43 | } 44 | } 45 | #endif 46 | if (use_header_bar) { 47 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); 48 | gtk_widget_show(GTK_WIDGET(header_bar)); 49 | gtk_header_bar_set_title(header_bar, "Enthusiast Tea Timer"); 50 | gtk_header_bar_set_show_close_button(header_bar, TRUE); 51 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); 52 | } else { 53 | gtk_window_set_title(window, "Enthusiast Tea Timer"); 54 | } 55 | 56 | gtk_window_set_default_size(window, 1280, 720); 57 | 58 | g_autoptr(FlDartProject) project = fl_dart_project_new(); 59 | fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); 60 | 61 | FlView* view = fl_view_new(project); 62 | GdkRGBA background_color; 63 | // Background defaults to black, override it here if necessary, e.g. #00000000 for transparent. 64 | gdk_rgba_parse(&background_color, "#000000"); 65 | fl_view_set_background_color(view, &background_color); 66 | gtk_widget_show(GTK_WIDGET(view)); 67 | gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); 68 | 69 | // Show the window when Flutter renders. 70 | // Requires the view to be realized so we can start rendering. 71 | g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), self); 72 | gtk_widget_realize(GTK_WIDGET(view)); 73 | 74 | fl_register_plugins(FL_PLUGIN_REGISTRY(view)); 75 | 76 | gtk_widget_grab_focus(GTK_WIDGET(view)); 77 | } 78 | 79 | // Implements GApplication::local_command_line. 80 | static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { 81 | MyApplication* self = MY_APPLICATION(application); 82 | // Strip out the first argument as it is the binary name. 83 | self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); 84 | 85 | g_autoptr(GError) error = nullptr; 86 | if (!g_application_register(application, nullptr, &error)) { 87 | g_warning("Failed to register: %s", error->message); 88 | *exit_status = 1; 89 | return TRUE; 90 | } 91 | 92 | g_application_activate(application); 93 | *exit_status = 0; 94 | 95 | return TRUE; 96 | } 97 | 98 | // Implements GApplication::startup. 99 | static void my_application_startup(GApplication* application) { 100 | //MyApplication* self = MY_APPLICATION(object); 101 | 102 | // Perform any actions required at application startup. 103 | 104 | G_APPLICATION_CLASS(my_application_parent_class)->startup(application); 105 | } 106 | 107 | // Implements GApplication::shutdown. 108 | static void my_application_shutdown(GApplication* application) { 109 | //MyApplication* self = MY_APPLICATION(object); 110 | 111 | // Perform any actions required at application shutdown. 112 | 113 | G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); 114 | } 115 | 116 | // Implements GObject::dispose. 117 | static void my_application_dispose(GObject* object) { 118 | MyApplication* self = MY_APPLICATION(object); 119 | g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); 120 | G_OBJECT_CLASS(my_application_parent_class)->dispose(object); 121 | } 122 | 123 | static void my_application_class_init(MyApplicationClass* klass) { 124 | G_APPLICATION_CLASS(klass)->activate = my_application_activate; 125 | G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; 126 | G_APPLICATION_CLASS(klass)->startup = my_application_startup; 127 | G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; 128 | G_OBJECT_CLASS(klass)->dispose = my_application_dispose; 129 | } 130 | 131 | static void my_application_init(MyApplication* self) {} 132 | 133 | MyApplication* my_application_new() { 134 | // Set the program name to the application ID, which helps various systems 135 | // like GTK and desktop environments map this running application to its 136 | // corresponding .desktop file. This ensures better integration by allowing 137 | // the application to be recognized beyond its binary name. 138 | g_set_prgname(APPLICATION_ID); 139 | 140 | return MY_APPLICATION(g_object_new(my_application_get_type(), 141 | "application-id", APPLICATION_ID, 142 | "flags", G_APPLICATION_NON_UNIQUE, 143 | nullptr)); 144 | } 145 | -------------------------------------------------------------------------------- /linux/runner/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import audioplayers_darwin 9 | import flutter_local_notifications 10 | import flutter_volume_controller 11 | import package_info_plus 12 | import path_provider_foundation 13 | import share_plus 14 | import shared_preferences_foundation 15 | 16 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 17 | AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) 18 | FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) 19 | FlutterVolumeControllerPlugin.register(with: registry.registrar(forPlugin: "FlutterVolumeControllerPlugin")) 20 | FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 21 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 22 | SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) 23 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 24 | } 25 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 64 | 66 | 72 | 73 | 74 | 75 | 81 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | 10 | override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 11 | return true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = Enthusiast Tea Timer 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.sesu8642.infusion_timer 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /metadata/de-DE/full_description.txt: -------------------------------------------------------------------------------- 1 | Verwalte bequem Deine Teesammlung. Füge ganz nach Deinem Geschmack Aufgusszeiten und Zubereitungsanweisungen hinzu - sei es Gong Fu Cha oder westliche Teezubereitung. Für die gängigsten Teesorten gibt es praktische Voreinstellungen. Halte deine Erfahrungen beim Teegenuss in Verkostungsnotizen fest und vergib Sternebewertungen. Das Beste daran: Es ist Frei, Open Source und von Grund auf privatsphärefreundlich konzipiert. Keine Werbung, keine Datensammlung. Auf die perfekte Tasse Tee! -------------------------------------------------------------------------------- /metadata/de-DE/short_description.txt: -------------------------------------------------------------------------------- 1 | Tee-Timer für Gong Fu oder westliche Teezubereitung für echte Enthusiasten. 2 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/100.txt: -------------------------------------------------------------------------------- 1 | Initial Release -------------------------------------------------------------------------------- /metadata/en-US/changelogs/101.txt: -------------------------------------------------------------------------------- 1 | adjusted metadata for F-Droid release -------------------------------------------------------------------------------- /metadata/en-US/changelogs/102.txt: -------------------------------------------------------------------------------- 1 | update gradle wrapper (+audioplayers dependency) -------------------------------------------------------------------------------- /metadata/en-US/changelogs/103.txt: -------------------------------------------------------------------------------- 1 | removed unused RECEIVE_BOOT_COMPLETED permission 2 | added SCHEDULE_EXACT_ALARM for android 12 exact alarm scheduling -------------------------------------------------------------------------------- /metadata/en-US/changelogs/104.txt: -------------------------------------------------------------------------------- 1 | brewing progress notification (Linux: only a single notification when finished) 2 | changed display name to "Infusion Tea Timer" 3 | other minor fixes and improvements -------------------------------------------------------------------------------- /metadata/en-US/changelogs/105.txt: -------------------------------------------------------------------------------- 1 | fixed being unable to input decimal numbers for the tea amount 2 | fixed audio not playing on Android 12 3 | fixed audio delay / not playing at all when the screen is off on some devices -------------------------------------------------------------------------------- /metadata/en-US/changelogs/106.txt: -------------------------------------------------------------------------------- 1 | improved tea timer page UI 2 | added ability to continue a brewing session later -------------------------------------------------------------------------------- /metadata/en-US/changelogs/107.txt: -------------------------------------------------------------------------------- 1 | improved tea timer page UI 2 | added ability to continue a brewing session later -------------------------------------------------------------------------------- /metadata/en-US/changelogs/108.txt: -------------------------------------------------------------------------------- 1 | fixed session progress sometimes not being deleted 2 | fixed alarm sometimes not happening until the screen is turned on 3 | fixed crash message when the app is removed from recents during an infusion -------------------------------------------------------------------------------- /metadata/en-US/changelogs/109.txt: -------------------------------------------------------------------------------- 1 | improved timer accuracy and reliability by running in the background 2 | added collection search/filter functionality 3 | added data backup & restore functionality 4 | minor UI improvements to tea input dialog -------------------------------------------------------------------------------- /metadata/en-US/changelogs/110.txt: -------------------------------------------------------------------------------- 1 | improved timer accuracy and reliability by running in the background 2 | added collection search/filter functionality 3 | added data backup & restore functionality 4 | minor UI improvements to tea input dialog -------------------------------------------------------------------------------- /metadata/en-US/changelogs/111.txt: -------------------------------------------------------------------------------- 1 | renamed to "Enthusiast Tea Timer" 2 | added star rating for teas 3 | improved support for newer Android versions 4 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/112.txt: -------------------------------------------------------------------------------- 1 | switched to Material Design 3 2 | added confirmation dialog before cancelling infusions 3 | added warning when phone is muted 4 | added ability to quickly reset brewing session 5 | added detailed notes for teas 6 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/113.txt: -------------------------------------------------------------------------------- 1 | Enthusiast Tea Timer 1.6.0 2 | 3 | - new tea sharing feature 4 | - new option to copy teas 5 | - edge-to-edge mode on newer Android versions 6 | - the user is now asked whether to coninue a previous session or not 7 | -------------------------------------------------------------------------------- /metadata/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Effortlessly organize your tea collection, complete with infusion times and brewing instructions tailored to your taste - whether you prefer Gong Fu Cha or Western style. Enjoy handy presets for popular tea varieties. Take tasting notes, and give star ratings to your teas. Best of all, it's free, open-source, and designed with your privacy in mind - no ads, no data collection. Cheers to your perfect cup! -------------------------------------------------------------------------------- /metadata/en-US/images/featureGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/metadata/en-US/images/featureGraphic.png -------------------------------------------------------------------------------- /metadata/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/metadata/en-US/images/icon.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/metadata/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/metadata/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/metadata/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /metadata/en-US/images/sevenInchScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/metadata/en-US/images/sevenInchScreenshots/1.png -------------------------------------------------------------------------------- /metadata/en-US/images/sevenInchScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/metadata/en-US/images/sevenInchScreenshots/2.png -------------------------------------------------------------------------------- /metadata/en-US/images/sevenInchScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/metadata/en-US/images/sevenInchScreenshots/3.png -------------------------------------------------------------------------------- /metadata/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Tea timer for Gong Fu and western style brewing for true enthusiasts. -------------------------------------------------------------------------------- /metadata/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Enthusiast Tea Timer 2 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: infusion_timer 2 | description: "A new Flutter project." 3 | 4 | publish_to: 'none' 5 | 6 | version: 1.6.0+113 7 | 8 | environment: 9 | sdk: ^3.9.0 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | shared_preferences: ^2.2.2 15 | liquid_progress_indicator_v2: ^0.5.0 16 | cupertino_icons: ^1.0.8 17 | audioplayers: ^6.5.0 18 | android_alarm_manager_plus: ^4.0.8 19 | flutter_launcher_icons: ^0.14.4 20 | path_provider: ^2.1.1 21 | package_info_plus: ^8.3.1 22 | flutter_local_notifications: ^19.4.0 23 | flutter_background: ^1.1.0 24 | flutter_volume_controller: ^1.3.1 25 | share_plus: ^11.1.0 26 | 27 | dev_dependencies: 28 | flutter_test: 29 | sdk: flutter 30 | flutter_lints: ^5.0.0 31 | 32 | flutter: 33 | uses-material-design: true 34 | default-flavor: default 35 | 36 | assets: 37 | - "assets/hand-bell-ringing-sound.wav" 38 | - "assets/icon_simple_512.png" 39 | - "assets/default_data.json" 40 | -------------------------------------------------------------------------------- /test/backup_data_validation_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:infusion_timer/backup_data.dart'; 3 | import 'package:infusion_timer/tea.dart'; 4 | 5 | late BackupData _sut; 6 | Tea _validTea = Tea(0.1, "Some Tea", 10, 1, [Infusion(60), Infusion(120)], 7 | "This is just some tea", "Notes...", 1); 8 | 9 | void main() { 10 | group('Validation', () { 11 | test('valid backup with teas is valid', () { 12 | _sut = BackupData(100, [_validTea], {0.1: 2}); 13 | _sut.validate(); 14 | }); 15 | 16 | test('valid backup without teas is valid', () { 17 | _sut = BackupData(100, [], {}); 18 | _sut.validate(); 19 | }); 20 | 21 | test('null tea vessel size is invalid', () { 22 | _sut = BackupData(null, [], {}); 23 | expect(() => _sut.validate(), throwsA(isA())); 24 | }); 25 | 26 | test('null teas is invalid', () { 27 | _sut = BackupData(100, null, {}); 28 | expect(() => _sut.validate(), throwsA(isA())); 29 | }); 30 | 31 | test('invalid tea is invalid', () { 32 | _sut = 33 | BackupData(100, [Tea(1, null, null, null, [], null, null, null)], {}); 34 | expect(() => _sut.validate(), throwsA(isA())); 35 | }); 36 | 37 | test('non-unique tea id is invalid', () { 38 | _sut = BackupData(100, [ 39 | Tea(0.1, "Some other Tea", 10, 1, [Infusion(60)], 40 | "Some other tea with the same id", "Also some Notes", 3), 41 | _validTea 42 | ], {}); 43 | expect(() => _sut.validate(), throwsA(isA())); 44 | }); 45 | 46 | test('negative session index is invalid', () { 47 | _sut = BackupData(100, [_validTea], {0.1: -1}); 48 | expect(() => _sut.validate(), throwsA(isA())); 49 | }); 50 | 51 | test('positive but too small session index is invalid', () { 52 | _sut = BackupData(100, [_validTea], {0.1: 1}); 53 | expect(() => _sut.validate(), throwsA(isA())); 54 | }); 55 | 56 | test('too large session index is invalid', () { 57 | _sut = BackupData(100, [_validTea], {0.1: 3}); 58 | expect(() => _sut.validate(), throwsA(isA())); 59 | }); 60 | 61 | test('session without matching tea is invalid', () { 62 | _sut = BackupData(100, [_validTea], {0.2: 2}); 63 | expect(() => _sut.validate(), throwsA(isA())); 64 | }); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /test/tea_validation_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:infusion_timer/tea.dart'; 3 | 4 | late Tea _sut; 5 | 6 | void main() { 7 | setUp(() { 8 | // start with a valid tea 9 | _sut = Tea(0.012345, "Some Tea", 10, 1, [Infusion(60)], 10 | "This is just some tea", "Notes...", 2); 11 | }); 12 | 13 | group('Validation', () { 14 | test('valid tea is valid', () { 15 | _sut.validate(); 16 | }); 17 | 18 | test('null name is invalid', () { 19 | _sut.name = null; 20 | expect(() => _sut.validate(), throwsA(isA())); 21 | }); 22 | 23 | test('null temperature is invalid', () { 24 | _sut.temperature = null; 25 | expect(() => _sut.validate(), throwsA(isA())); 26 | }); 27 | 28 | test('null gPer100Ml is invalid', () { 29 | _sut.gPer100Ml = null; 30 | expect(() => _sut.validate(), throwsA(isA())); 31 | }); 32 | 33 | test('empty infusions is invalid', () { 34 | _sut.infusions = []; 35 | expect(() => _sut.validate(), throwsA(isA())); 36 | }); 37 | 38 | test('negative infusion time is invalid', () { 39 | _sut.infusions = [Infusion(-1)]; 40 | expect(() => _sut.validate(), throwsA(isA())); 41 | }); 42 | 43 | test('null subtitle is invalid', () { 44 | _sut.subtitle = null; 45 | expect(() => _sut.validate(), throwsA(isA())); 46 | }); 47 | 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Enthusiast Tea Timer 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "infusion_timer", 3 | "short_name": "infusion_timer", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.14) 3 | project(infusion_timer LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "enthusiast_tea_timer") 8 | 9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 10 | # versions of CMake. 11 | cmake_policy(VERSION 3.14...3.25) 12 | 13 | # Define build configuration option. 14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 15 | if(IS_MULTICONFIG) 16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 17 | CACHE STRING "" FORCE) 18 | else() 19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 20 | set(CMAKE_BUILD_TYPE "Debug" CACHE 21 | STRING "Flutter build mode" FORCE) 22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 23 | "Debug" "Profile" "Release") 24 | endif() 25 | endif() 26 | # Define settings for the Profile build mode. 27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 31 | 32 | # Use Unicode for all projects. 33 | add_definitions(-DUNICODE -D_UNICODE) 34 | 35 | # Compilation settings that should be applied to most targets. 36 | # 37 | # Be cautious about adding new options here, as plugins use this function by 38 | # default. In most cases, you should add new options to specific targets instead 39 | # of modifying this function. 40 | function(APPLY_STANDARD_SETTINGS TARGET) 41 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 43 | target_compile_options(${TARGET} PRIVATE /EHsc) 44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 46 | endfunction() 47 | 48 | # Flutter library and tool build rules. 49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 50 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 51 | 52 | # Application build; see runner/CMakeLists.txt. 53 | add_subdirectory("runner") 54 | 55 | 56 | # Generated plugin build rules, which manage building the plugins and adding 57 | # them to the application. 58 | include(flutter/generated_plugins.cmake) 59 | 60 | 61 | # === Installation === 62 | # Support files are copied into place next to the executable, so that it can 63 | # run in place. This is done instead of making a separate bundle (as on Linux) 64 | # so that building and running from within Visual Studio will work. 65 | set(BUILD_BUNDLE_DIR "$") 66 | # Make the "install" step default, as it's required to run. 67 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 68 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 69 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 70 | endif() 71 | 72 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 73 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 74 | 75 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 76 | COMPONENT Runtime) 77 | 78 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 79 | COMPONENT Runtime) 80 | 81 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 82 | COMPONENT Runtime) 83 | 84 | if(PLUGIN_BUNDLED_LIBRARIES) 85 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 86 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 87 | COMPONENT Runtime) 88 | endif() 89 | 90 | # Copy the native assets provided by the build.dart from all packages. 91 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") 92 | install(DIRECTORY "${NATIVE_ASSETS_DIR}" 93 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 94 | COMPONENT Runtime) 95 | 96 | # Fully re-copy the assets directory on each build to avoid having stale files 97 | # from a previous install. 98 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 99 | install(CODE " 100 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 101 | " COMPONENT Runtime) 102 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 103 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 104 | 105 | # Install the AOT library on non-Debug builds only. 106 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 107 | CONFIGURATIONS Profile;Release 108 | COMPONENT Runtime) 109 | -------------------------------------------------------------------------------- /windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.14) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 12 | 13 | # Set fallback configurations for older versions of the flutter tool. 14 | if (NOT DEFINED FLUTTER_TARGET_PLATFORM) 15 | set(FLUTTER_TARGET_PLATFORM "windows-x64") 16 | endif() 17 | 18 | # === Flutter Library === 19 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 20 | 21 | # Published to parent scope for install step. 22 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 23 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 24 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 25 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 26 | 27 | list(APPEND FLUTTER_LIBRARY_HEADERS 28 | "flutter_export.h" 29 | "flutter_windows.h" 30 | "flutter_messenger.h" 31 | "flutter_plugin_registrar.h" 32 | "flutter_texture_registrar.h" 33 | ) 34 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 35 | add_library(flutter INTERFACE) 36 | target_include_directories(flutter INTERFACE 37 | "${EPHEMERAL_DIR}" 38 | ) 39 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 40 | add_dependencies(flutter flutter_assemble) 41 | 42 | # === Wrapper === 43 | list(APPEND CPP_WRAPPER_SOURCES_CORE 44 | "core_implementations.cc" 45 | "standard_codec.cc" 46 | ) 47 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 48 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 49 | "plugin_registrar.cc" 50 | ) 51 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 52 | list(APPEND CPP_WRAPPER_SOURCES_APP 53 | "flutter_engine.cc" 54 | "flutter_view_controller.cc" 55 | ) 56 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 57 | 58 | # Wrapper sources needed for a plugin. 59 | add_library(flutter_wrapper_plugin STATIC 60 | ${CPP_WRAPPER_SOURCES_CORE} 61 | ${CPP_WRAPPER_SOURCES_PLUGIN} 62 | ) 63 | apply_standard_settings(flutter_wrapper_plugin) 64 | set_target_properties(flutter_wrapper_plugin PROPERTIES 65 | POSITION_INDEPENDENT_CODE ON) 66 | set_target_properties(flutter_wrapper_plugin PROPERTIES 67 | CXX_VISIBILITY_PRESET hidden) 68 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 69 | target_include_directories(flutter_wrapper_plugin PUBLIC 70 | "${WRAPPER_ROOT}/include" 71 | ) 72 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 73 | 74 | # Wrapper sources needed for the runner. 75 | add_library(flutter_wrapper_app STATIC 76 | ${CPP_WRAPPER_SOURCES_CORE} 77 | ${CPP_WRAPPER_SOURCES_APP} 78 | ) 79 | apply_standard_settings(flutter_wrapper_app) 80 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 81 | target_include_directories(flutter_wrapper_app PUBLIC 82 | "${WRAPPER_ROOT}/include" 83 | ) 84 | add_dependencies(flutter_wrapper_app flutter_assemble) 85 | 86 | # === Flutter tool backend === 87 | # _phony_ is a non-existent file to force this command to run every time, 88 | # since currently there's no way to get a full input/output list from the 89 | # flutter tool. 90 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 91 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 92 | add_custom_command( 93 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 94 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 95 | ${CPP_WRAPPER_SOURCES_APP} 96 | ${PHONY_OUTPUT} 97 | COMMAND ${CMAKE_COMMAND} -E env 98 | ${FLUTTER_TOOL_ENVIRONMENT} 99 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 100 | ${FLUTTER_TARGET_PLATFORM} $ 101 | VERBATIM 102 | ) 103 | add_custom_target(flutter_assemble DEPENDS 104 | "${FLUTTER_LIBRARY}" 105 | ${FLUTTER_LIBRARY_HEADERS} 106 | ${CPP_WRAPPER_SOURCES_CORE} 107 | ${CPP_WRAPPER_SOURCES_PLUGIN} 108 | ${CPP_WRAPPER_SOURCES_APP} 109 | ) 110 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | void RegisterPlugins(flutter::PluginRegistry* registry) { 15 | AudioplayersWindowsPluginRegisterWithRegistrar( 16 | registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); 17 | FlutterVolumeControllerPluginCApiRegisterWithRegistrar( 18 | registry->GetRegistrarForPlugin("FlutterVolumeControllerPluginCApi")); 19 | SharePlusWindowsPluginCApiRegisterWithRegistrar( 20 | registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); 21 | UrlLauncherWindowsRegisterWithRegistrar( 22 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 23 | } 24 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | audioplayers_windows 7 | flutter_volume_controller 8 | share_plus 9 | url_launcher_windows 10 | ) 11 | 12 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 13 | flutter_local_notifications_windows 14 | ) 15 | 16 | set(PLUGIN_BUNDLED_LIBRARIES) 17 | 18 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 19 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 20 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 21 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 23 | endforeach(plugin) 24 | 25 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 26 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 27 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 28 | endforeach(ffi_plugin) 29 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) 64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0,0 67 | #endif 68 | 69 | #if defined(FLUTTER_VERSION) 70 | #define VERSION_AS_STRING FLUTTER_VERSION 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.example" "\0" 93 | VALUE "FileDescription", "infusion_timer" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "infusion_timer" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "infusion_timer.exe" "\0" 98 | VALUE "ProductName", "infusion_timer" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | this->Show(); 32 | }); 33 | 34 | // Flutter can complete the first frame before the "show window" callback is 35 | // registered. The following call ensures a frame is pending to ensure the 36 | // window is shown. It is a no-op if the first frame hasn't completed yet. 37 | flutter_controller_->ForceRedraw(); 38 | 39 | return true; 40 | } 41 | 42 | void FlutterWindow::OnDestroy() { 43 | if (flutter_controller_) { 44 | flutter_controller_ = nullptr; 45 | } 46 | 47 | Win32Window::OnDestroy(); 48 | } 49 | 50 | LRESULT 51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 52 | WPARAM const wparam, 53 | LPARAM const lparam) noexcept { 54 | // Give Flutter, including plugins, an opportunity to handle window messages. 55 | if (flutter_controller_) { 56 | std::optional result = 57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 58 | lparam); 59 | if (result) { 60 | return *result; 61 | } 62 | } 63 | 64 | switch (message) { 65 | case WM_FONTCHANGE: 66 | flutter_controller_->engine()->ReloadSystemFonts(); 67 | break; 68 | } 69 | 70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 71 | } 72 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"Enthusiast Tea Timer", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sesu8642/InfusionTimer/41ec303461b29d8b4315a95094b2db16b5d19b3a/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | unsigned int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length == 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /windows/runner/win32_window.cpp: -------------------------------------------------------------------------------- 1 | #include "win32_window.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "resource.h" 7 | 8 | namespace { 9 | 10 | /// Window attribute that enables dark mode window decorations. 11 | /// 12 | /// Redefined in case the developer's machine has a Windows SDK older than 13 | /// version 10.0.22000.0. 14 | /// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute 15 | #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE 16 | #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 17 | #endif 18 | 19 | constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; 20 | 21 | /// Registry key for app theme preference. 22 | /// 23 | /// A value of 0 indicates apps should use dark mode. A non-zero or missing 24 | /// value indicates apps should use light mode. 25 | constexpr const wchar_t kGetPreferredBrightnessRegKey[] = 26 | L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; 27 | constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; 28 | 29 | // The number of Win32Window objects that currently exist. 30 | static int g_active_window_count = 0; 31 | 32 | using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); 33 | 34 | // Scale helper to convert logical scaler values to physical using passed in 35 | // scale factor 36 | int Scale(int source, double scale_factor) { 37 | return static_cast(source * scale_factor); 38 | } 39 | 40 | // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. 41 | // This API is only needed for PerMonitor V1 awareness mode. 42 | void EnableFullDpiSupportIfAvailable(HWND hwnd) { 43 | HMODULE user32_module = LoadLibraryA("User32.dll"); 44 | if (!user32_module) { 45 | return; 46 | } 47 | auto enable_non_client_dpi_scaling = 48 | reinterpret_cast( 49 | GetProcAddress(user32_module, "EnableNonClientDpiScaling")); 50 | if (enable_non_client_dpi_scaling != nullptr) { 51 | enable_non_client_dpi_scaling(hwnd); 52 | } 53 | FreeLibrary(user32_module); 54 | } 55 | 56 | } // namespace 57 | 58 | // Manages the Win32Window's window class registration. 59 | class WindowClassRegistrar { 60 | public: 61 | ~WindowClassRegistrar() = default; 62 | 63 | // Returns the singleton registrar instance. 64 | static WindowClassRegistrar* GetInstance() { 65 | if (!instance_) { 66 | instance_ = new WindowClassRegistrar(); 67 | } 68 | return instance_; 69 | } 70 | 71 | // Returns the name of the window class, registering the class if it hasn't 72 | // previously been registered. 73 | const wchar_t* GetWindowClass(); 74 | 75 | // Unregisters the window class. Should only be called if there are no 76 | // instances of the window. 77 | void UnregisterWindowClass(); 78 | 79 | private: 80 | WindowClassRegistrar() = default; 81 | 82 | static WindowClassRegistrar* instance_; 83 | 84 | bool class_registered_ = false; 85 | }; 86 | 87 | WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; 88 | 89 | const wchar_t* WindowClassRegistrar::GetWindowClass() { 90 | if (!class_registered_) { 91 | WNDCLASS window_class{}; 92 | window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); 93 | window_class.lpszClassName = kWindowClassName; 94 | window_class.style = CS_HREDRAW | CS_VREDRAW; 95 | window_class.cbClsExtra = 0; 96 | window_class.cbWndExtra = 0; 97 | window_class.hInstance = GetModuleHandle(nullptr); 98 | window_class.hIcon = 99 | LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); 100 | window_class.hbrBackground = 0; 101 | window_class.lpszMenuName = nullptr; 102 | window_class.lpfnWndProc = Win32Window::WndProc; 103 | RegisterClass(&window_class); 104 | class_registered_ = true; 105 | } 106 | return kWindowClassName; 107 | } 108 | 109 | void WindowClassRegistrar::UnregisterWindowClass() { 110 | UnregisterClass(kWindowClassName, nullptr); 111 | class_registered_ = false; 112 | } 113 | 114 | Win32Window::Win32Window() { 115 | ++g_active_window_count; 116 | } 117 | 118 | Win32Window::~Win32Window() { 119 | --g_active_window_count; 120 | Destroy(); 121 | } 122 | 123 | bool Win32Window::Create(const std::wstring& title, 124 | const Point& origin, 125 | const Size& size) { 126 | Destroy(); 127 | 128 | const wchar_t* window_class = 129 | WindowClassRegistrar::GetInstance()->GetWindowClass(); 130 | 131 | const POINT target_point = {static_cast(origin.x), 132 | static_cast(origin.y)}; 133 | HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); 134 | UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); 135 | double scale_factor = dpi / 96.0; 136 | 137 | HWND window = CreateWindow( 138 | window_class, title.c_str(), WS_OVERLAPPEDWINDOW, 139 | Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), 140 | Scale(size.width, scale_factor), Scale(size.height, scale_factor), 141 | nullptr, nullptr, GetModuleHandle(nullptr), this); 142 | 143 | if (!window) { 144 | return false; 145 | } 146 | 147 | UpdateTheme(window); 148 | 149 | return OnCreate(); 150 | } 151 | 152 | bool Win32Window::Show() { 153 | return ShowWindow(window_handle_, SW_SHOWNORMAL); 154 | } 155 | 156 | // static 157 | LRESULT CALLBACK Win32Window::WndProc(HWND const window, 158 | UINT const message, 159 | WPARAM const wparam, 160 | LPARAM const lparam) noexcept { 161 | if (message == WM_NCCREATE) { 162 | auto window_struct = reinterpret_cast(lparam); 163 | SetWindowLongPtr(window, GWLP_USERDATA, 164 | reinterpret_cast(window_struct->lpCreateParams)); 165 | 166 | auto that = static_cast(window_struct->lpCreateParams); 167 | EnableFullDpiSupportIfAvailable(window); 168 | that->window_handle_ = window; 169 | } else if (Win32Window* that = GetThisFromHandle(window)) { 170 | return that->MessageHandler(window, message, wparam, lparam); 171 | } 172 | 173 | return DefWindowProc(window, message, wparam, lparam); 174 | } 175 | 176 | LRESULT 177 | Win32Window::MessageHandler(HWND hwnd, 178 | UINT const message, 179 | WPARAM const wparam, 180 | LPARAM const lparam) noexcept { 181 | switch (message) { 182 | case WM_DESTROY: 183 | window_handle_ = nullptr; 184 | Destroy(); 185 | if (quit_on_close_) { 186 | PostQuitMessage(0); 187 | } 188 | return 0; 189 | 190 | case WM_DPICHANGED: { 191 | auto newRectSize = reinterpret_cast(lparam); 192 | LONG newWidth = newRectSize->right - newRectSize->left; 193 | LONG newHeight = newRectSize->bottom - newRectSize->top; 194 | 195 | SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, 196 | newHeight, SWP_NOZORDER | SWP_NOACTIVATE); 197 | 198 | return 0; 199 | } 200 | case WM_SIZE: { 201 | RECT rect = GetClientArea(); 202 | if (child_content_ != nullptr) { 203 | // Size and position the child window. 204 | MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, 205 | rect.bottom - rect.top, TRUE); 206 | } 207 | return 0; 208 | } 209 | 210 | case WM_ACTIVATE: 211 | if (child_content_ != nullptr) { 212 | SetFocus(child_content_); 213 | } 214 | return 0; 215 | 216 | case WM_DWMCOLORIZATIONCOLORCHANGED: 217 | UpdateTheme(hwnd); 218 | return 0; 219 | } 220 | 221 | return DefWindowProc(window_handle_, message, wparam, lparam); 222 | } 223 | 224 | void Win32Window::Destroy() { 225 | OnDestroy(); 226 | 227 | if (window_handle_) { 228 | DestroyWindow(window_handle_); 229 | window_handle_ = nullptr; 230 | } 231 | if (g_active_window_count == 0) { 232 | WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); 233 | } 234 | } 235 | 236 | Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { 237 | return reinterpret_cast( 238 | GetWindowLongPtr(window, GWLP_USERDATA)); 239 | } 240 | 241 | void Win32Window::SetChildContent(HWND content) { 242 | child_content_ = content; 243 | SetParent(content, window_handle_); 244 | RECT frame = GetClientArea(); 245 | 246 | MoveWindow(content, frame.left, frame.top, frame.right - frame.left, 247 | frame.bottom - frame.top, true); 248 | 249 | SetFocus(child_content_); 250 | } 251 | 252 | RECT Win32Window::GetClientArea() { 253 | RECT frame; 254 | GetClientRect(window_handle_, &frame); 255 | return frame; 256 | } 257 | 258 | HWND Win32Window::GetHandle() { 259 | return window_handle_; 260 | } 261 | 262 | void Win32Window::SetQuitOnClose(bool quit_on_close) { 263 | quit_on_close_ = quit_on_close; 264 | } 265 | 266 | bool Win32Window::OnCreate() { 267 | // No-op; provided for subclasses. 268 | return true; 269 | } 270 | 271 | void Win32Window::OnDestroy() { 272 | // No-op; provided for subclasses. 273 | } 274 | 275 | void Win32Window::UpdateTheme(HWND const window) { 276 | DWORD light_mode; 277 | DWORD light_mode_size = sizeof(light_mode); 278 | LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, 279 | kGetPreferredBrightnessRegValue, 280 | RRF_RT_REG_DWORD, nullptr, &light_mode, 281 | &light_mode_size); 282 | 283 | if (result == ERROR_SUCCESS) { 284 | BOOL enable_dark_mode = light_mode == 0; 285 | DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, 286 | &enable_dark_mode, sizeof(enable_dark_mode)); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates a win32 window with |title| that is positioned and sized using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size this function will scale the inputted width and height as 35 | // as appropriate for the default monitor. The window is invisible until 36 | // |Show| is called. Returns true if the window was created successfully. 37 | bool Create(const std::wstring& title, const Point& origin, const Size& size); 38 | 39 | // Show the current window. Returns true if the window was successfully shown. 40 | bool Show(); 41 | 42 | // Release OS resources associated with window. 43 | void Destroy(); 44 | 45 | // Inserts |content| into the window tree. 46 | void SetChildContent(HWND content); 47 | 48 | // Returns the backing Window handle to enable clients to set icon and other 49 | // window properties. Returns nullptr if the window has been destroyed. 50 | HWND GetHandle(); 51 | 52 | // If true, closing this window will quit the application. 53 | void SetQuitOnClose(bool quit_on_close); 54 | 55 | // Return a RECT representing the bounds of the current client area. 56 | RECT GetClientArea(); 57 | 58 | protected: 59 | // Processes and route salient window messages for mouse handling, 60 | // size change and DPI. Delegates handling of these to member overloads that 61 | // inheriting classes can handle. 62 | virtual LRESULT MessageHandler(HWND window, 63 | UINT const message, 64 | WPARAM const wparam, 65 | LPARAM const lparam) noexcept; 66 | 67 | // Called when CreateAndShow is called, allowing subclass window-related 68 | // setup. Subclasses should return false if setup fails. 69 | virtual bool OnCreate(); 70 | 71 | // Called when Destroy is called. 72 | virtual void OnDestroy(); 73 | 74 | private: 75 | friend class WindowClassRegistrar; 76 | 77 | // OS callback called by message pump. Handles the WM_NCCREATE message which 78 | // is passed when the non-client area is being created and enables automatic 79 | // non-client DPI scaling so that the non-client area automatically 80 | // responds to changes in DPI. All other messages are handled by 81 | // MessageHandler. 82 | static LRESULT CALLBACK WndProc(HWND const window, 83 | UINT const message, 84 | WPARAM const wparam, 85 | LPARAM const lparam) noexcept; 86 | 87 | // Retrieves a class instance pointer for |window| 88 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 89 | 90 | // Update the window frame's theme to match the system theme. 91 | static void UpdateTheme(HWND const window); 92 | 93 | bool quit_on_close_ = false; 94 | 95 | // window handle for top level window. 96 | HWND window_handle_ = nullptr; 97 | 98 | // window handle for hosted content. 99 | HWND child_content_ = nullptr; 100 | }; 101 | 102 | #endif // RUNNER_WIN32_WINDOW_H_ 103 | --------------------------------------------------------------------------------