├── .github └── workflows │ └── main.yml ├── .gitignore ├── .gitmodules ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── ic_launcher-playstore.png │ │ ├── kotlin │ │ │ └── bored │ │ │ │ └── codebyk │ │ │ │ └── mintcalc │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ ├── ic_launcher_foreground.xml │ │ │ ├── ic_launcher_monochrome.xml │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ ├── ic_launcher_monochrome.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ ├── ic_launcher_monochrome.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ ├── ic_launcher_monochrome.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ ├── ic_launcher_monochrome.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_background.png │ │ │ ├── ic_launcher_foreground.png │ │ │ ├── ic_launcher_monochrome.png │ │ │ └── ic_launcher_round.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ ├── ic_launcher_background.xml │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── github-mark-white.svg └── github-mark.svg ├── fonts └── Manrope │ └── Manrope-Regular.ttf ├── lib ├── main.dart ├── models │ └── settings_model.dart └── pages │ ├── calc │ ├── date_calc.dart │ ├── scientific_calc.dart │ └── standard_calc.dart │ ├── conv │ ├── angle_conv.dart │ ├── area_conv.dart │ ├── data_conv.dart │ ├── energy_conv.dart │ ├── length_conv.dart │ ├── mass_conv.dart │ ├── power_conv.dart │ ├── pressure_conv.dart │ ├── speed_conv.dart │ ├── temp_conv.dart │ ├── time_conv.dart │ └── volume_conv.dart │ ├── pages.dart │ └── settings_page.dart ├── metadata └── en-US │ ├── changelogs │ ├── 100.txt │ ├── 110.txt │ └── 111.txt │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ └── 3.png │ └── short_description.txt ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Release Build 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | workflow_dispatch: 8 | 9 | env: 10 | APK_BUILD_DIR: "/tmp/build" 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout the code 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup Java to compile Android project 19 | uses: actions/setup-java@v3 20 | with: 21 | distribution: 'temurin' 22 | java-version: '17' 23 | 24 | - name: Get version from pubspec.yaml 25 | id: get_version 26 | run: | 27 | VERSION=$(sed -n 's/^version: \([0-9]*\.[0-9]*\.[0-9]*\).*/\1/p' pubspec.yaml) 28 | echo "version=$VERSION" >> $GITHUB_OUTPUT 29 | 30 | - name: Copy files to env.APK_BUILD_DIR 31 | run: | 32 | mkdir -p $APK_BUILD_DIR 33 | cp -r . $APK_BUILD_DIR 34 | 35 | - name: Setup Flutter 36 | uses: subosito/flutter-action@v2 37 | with: 38 | channel: 'stable' 39 | 40 | - name: Flutter version 41 | run: | 42 | flutter config --no-analytics 43 | flutter --version 44 | 45 | - name: Decode key.properties file 46 | working-directory: ${{ env.APK_BUILD_DIR }} 47 | env: 48 | ENCODED_STRING: ${{ secrets.ANDROID_KEY_PROPERTIES }} 49 | run: echo $ENCODED_STRING | base64 -di > android/key.properties 50 | 51 | - name: Decode android-keystore.jks file 52 | working-directory: ${{ env.APK_BUILD_DIR }} 53 | env: 54 | ENCODED_STRING: ${{ secrets.KEY_JKS }} 55 | run: echo $ENCODED_STRING | base64 -di > android/key.jks 56 | 57 | - name: Dependencies 58 | working-directory: ${{ env.APK_BUILD_DIR }} 59 | run: flutter pub get 60 | 61 | - name: Build generated files 62 | working-directory: ${{ env.APK_BUILD_DIR }} 63 | run: flutter pub run build_runner build -d 64 | 65 | - name: Build APK 66 | working-directory: ${{ env.APK_BUILD_DIR }} 67 | run: flutter build apk --release 68 | 69 | - name: Upload artifacts 70 | uses: actions/upload-artifact@v3 71 | with: 72 | name: release-apk 73 | path: ${{ env.APK_BUILD_DIR }}/build/app/outputs/flutter-apk/app-release.apk 74 | 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 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 | 46 | *.jks 47 | /android/key.properties -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "flutter"] 2 | path = flutter 3 | url = https://github.com/flutter/flutter.git 4 | branch = stable 5 | -------------------------------------------------------------------------------- /.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. 5 | 6 | version: 7 | revision: 796c8ef79279f9c774545b3771238c3098dbefab 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 17 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 18 | - platform: android 19 | create_revision: 796c8ef79279f9c774545b3771238c3098dbefab 20 | base_revision: 796c8ef79279f9c774545b3771238c3098dbefab 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mint Calculator 2 | 3 | ![F-Droid (including pre-releases)](https://img.shields.io/f-droid/v/bored.codebyk.mintcalc?style=for-the-badge&logoColor=1a1e00&labelColor=1a1e00&color=ddeb77) 4 | 5 | ### **!! Notice: After v1.1.0 I'll be in hiatus for sometime writing this calculator from scratch and also bring scientific calculator. Meanwhile please report any bugs if you find and feature request are appreciated!** 6 | 7 | A simple calculator and unit converter app with Material Design 3 inspired by Windows Calculator 8 | 9 | [Get it on F-Droid](https://f-droid.org/en/packages/bored.codebyk.mintcalc/) 12 | 13 | 14 | ## Features 15 | 16 | - Standard Calculator 17 | - Date Calculator 18 | - Simple unit converter (Angle, Time, Data, Length, Area, Volume, etc...) 19 | 20 | ## Screenshots 21 | 22 | | Calculator | Converter | 23 | | ------------ | ------------ | 24 | |![](metadata/en-US/images/phoneScreenshots/1.png) |![](metadata/en-US/images/phoneScreenshots/2.png) | 25 | 26 | ## Planned updates 27 | 28 | - Scientific calculator -------------------------------------------------------------------------------- /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 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | def keystoreProperties = new Properties() 25 | def keystorePropertiesFile = rootProject.file('key.properties') 26 | if (keystorePropertiesFile.exists()) { 27 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 28 | } 29 | 30 | apply plugin: 'com.android.application' 31 | apply plugin: 'kotlin-android' 32 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 33 | 34 | android { 35 | namespace "bored.codebyk.mintcalc" 36 | compileSdkVersion flutter.compileSdkVersion 37 | ndkVersion flutter.ndkVersion 38 | 39 | compileOptions { 40 | sourceCompatibility JavaVersion.VERSION_1_8 41 | targetCompatibility JavaVersion.VERSION_1_8 42 | } 43 | 44 | kotlinOptions { 45 | jvmTarget = '1.8' 46 | } 47 | 48 | sourceSets { 49 | main.java.srcDirs += 'src/main/kotlin' 50 | } 51 | 52 | defaultConfig { 53 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 54 | applicationId "bored.codebyk.mintcalc" 55 | // You can update the following values to match your application needs. 56 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 57 | minSdkVersion 21 58 | targetSdkVersion 33 59 | versionCode flutterVersionCode.toInteger() 60 | versionName flutterVersionName 61 | } 62 | 63 | signingConfigs { 64 | release { 65 | keyAlias keystoreProperties['keyAlias'] 66 | keyPassword keystoreProperties['keyPassword'] 67 | storeFile = file("../key.jks") ? file("../key.jks") : null 68 | storePassword keystoreProperties['storePassword'] 69 | v1SigningEnabled true 70 | v2SigningEnabled true 71 | } 72 | } 73 | 74 | buildTypes { 75 | release { 76 | signingConfig signingConfigs.release 77 | } 78 | } 79 | } 80 | 81 | flutter { 82 | source '../..' 83 | } 84 | 85 | dependencies { 86 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 87 | } 88 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 16 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /android/app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/bored/codebyk/mintcalc/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package bored.codebyk.mintcalc 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageInfo 5 | import android.content.pm.PackageManager 6 | import androidx.annotation.NonNull 7 | import io.flutter.embedding.android.FlutterActivity 8 | import io.flutter.embedding.engine.FlutterEngine 9 | import io.flutter.plugin.common.MethodChannel 10 | 11 | import android.os.Build 12 | 13 | class MainActivity: FlutterActivity() { 14 | private var applicationContext: Context? = null 15 | private val CHANNEL = "bored.codebyk.mintcalc/androidversion" 16 | 17 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { 18 | super.configureFlutterEngine(flutterEngine) 19 | MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { 20 | call, result -> 21 | // This method is invoked on the main thread. 22 | // TODO 23 | if (call.method == "getAndroidVersion") { 24 | val android_V = getAndroidVersion() 25 | result.success(android_V) 26 | } else { 27 | result.notImplemented() 28 | } 29 | } 30 | } 31 | 32 | fun getAndroidVersion(): Int { 33 | return Build.VERSION.SDK_INT 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /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/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #E4E3D2 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: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.8.22' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:8.0.1' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.defaults.buildfeatures.buildconfig=true 5 | android.nonTransitiveRClass=false 6 | android.nonFinalResIds=false 7 | -------------------------------------------------------------------------------- /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.0-all.zip 6 | distributionSha256Sum=f30b29580fe11719087d698da23f3b0f0d04031d8995f7dd8275a31f7674dc01 -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /assets/github-mark-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/github-mark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fonts/Manrope/Manrope-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/fonts/Manrope/Manrope-Regular.ttf -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:animations/animations.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:dynamic_color/dynamic_color.dart'; 6 | 7 | import './pages/pages.dart'; 8 | import 'models/settings_model.dart'; 9 | 10 | void main() { 11 | WidgetsFlutterBinding.ensureInitialized(); 12 | final settingsmodel = SettingsModel(); 13 | settingsmodel.load(); 14 | 15 | runApp(MultiProvider(providers: [ 16 | ChangeNotifierProvider.value(value: settingsmodel), 17 | ], child: const MyApp())); 18 | } 19 | 20 | class MyApp extends StatelessWidget { 21 | const MyApp({super.key}); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | SettingsModel settings = Provider.of(context); 26 | SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); 27 | SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( 28 | statusBarColor: Colors.transparent, 29 | systemNavigationBarDividerColor: Colors.transparent, 30 | systemNavigationBarContrastEnforced: true, 31 | systemNavigationBarColor: Colors.transparent, 32 | )); 33 | 34 | final defaultLightColorScheme = ColorScheme.fromSeed( 35 | seedColor: const Color.fromARGB(255, 217, 229, 129)); 36 | 37 | final defaultDarkColorScheme = ColorScheme.fromSeed( 38 | seedColor: const Color.fromARGB(255, 217, 229, 129), 39 | brightness: Brightness.dark); 40 | 41 | return DynamicColorBuilder( 42 | builder: (lightColorScheme, darkColorScheme) { 43 | return MaterialApp( 44 | title: 'Mint Calc', 45 | theme: ThemeData( 46 | colorScheme: settings.isSystemColor 47 | ? lightColorScheme 48 | : defaultLightColorScheme, 49 | fontFamily: 'Manrope', 50 | useMaterial3: true, 51 | ), 52 | darkTheme: ThemeData( 53 | colorScheme: settings.isSystemColor 54 | ? darkColorScheme 55 | : defaultDarkColorScheme, 56 | fontFamily: 'Manrope', 57 | useMaterial3: true, 58 | ), 59 | themeMode: settings.themeMode, 60 | home: const MyHomePage(), 61 | ); 62 | }, 63 | ); 64 | } 65 | } 66 | 67 | class MyHomePage extends StatefulWidget { 68 | const MyHomePage({super.key}); 69 | 70 | @override 71 | State createState() => _MyHomePageState(); 72 | } 73 | 74 | class _MyHomePageState extends State { 75 | static const List _pages = [ 76 | StdCalc(), 77 | SciCalc(), 78 | DateCalc(), 79 | AngleConv(), 80 | TemperatureConv(), 81 | DataConv(), 82 | TimeConv(), 83 | AreaConv(), 84 | LengthConv(), 85 | VolumeConv(), 86 | MassConv(), 87 | PressureConv(), 88 | SpeedConv(), 89 | PowerConv(), 90 | EnergyConv(), 91 | ]; 92 | final _pageTitles = { 93 | StdCalc: StdCalc.pageTitle, 94 | SciCalc: SciCalc.pageTitle, 95 | DateCalc: DateCalc.pageTitle, 96 | AngleConv: AngleConv.pageTitle, 97 | TemperatureConv: TemperatureConv.pageTitle, 98 | DataConv: DataConv.pageTitle, 99 | TimeConv: TimeConv.pageTitle, 100 | AreaConv: AreaConv.pageTitle, 101 | LengthConv: LengthConv.pageTitle, 102 | VolumeConv: VolumeConv.pageTitle, 103 | MassConv: MassConv.pageTitle, 104 | PressureConv: PressureConv.pageTitle, 105 | SpeedConv: SpeedConv.pageTitle, 106 | PowerConv: PowerConv.pageTitle, 107 | EnergyConv: EnergyConv.pageTitle 108 | }; 109 | int selectedIndex = 0; 110 | 111 | Route _createRoute(Widget widget) { 112 | return PageRouteBuilder( 113 | pageBuilder: (context, animation, secondaryAnimation) => widget, 114 | transitionsBuilder: (context, animation, secondaryAnimation, child) { 115 | return SharedAxisTransition( 116 | animation: animation, 117 | secondaryAnimation: secondaryAnimation, 118 | transitionType: SharedAxisTransitionType.horizontal, 119 | child: child, 120 | ); 121 | }, 122 | ); 123 | } 124 | 125 | @override 126 | Widget build(BuildContext context) { 127 | String appBarText = _pageTitles[_pages[selectedIndex].runtimeType] ?? ''; 128 | return Scaffold( 129 | appBar: AppBar( 130 | title: Text(appBarText), 131 | actions: [ 132 | IconButton( 133 | onPressed: () => 134 | Navigator.push(context, _createRoute(SettingsPage())), 135 | icon: const Icon(Icons.settings_outlined), 136 | ), 137 | ], 138 | ), 139 | body: _pages.elementAt(selectedIndex), 140 | drawer: NavigationDrawer( 141 | selectedIndex: selectedIndex, 142 | onDestinationSelected: (value) => setState(() { 143 | selectedIndex = value; 144 | Navigator.pop(context); 145 | }), 146 | children: [ 147 | Padding( 148 | padding: const EdgeInsets.fromLTRB(28, 16, 16, 10), 149 | child: Text( 150 | 'Calculator', 151 | style: Theme.of(context).textTheme.titleSmall, 152 | ), 153 | ), 154 | const NavigationDrawerDestination( 155 | icon: Icon(Icons.calculate_outlined), 156 | selectedIcon: Icon(Icons.calculate), 157 | label: Text("Standard"), 158 | ), 159 | const NavigationDrawerDestination( 160 | icon: Icon(Icons.science_outlined), 161 | selectedIcon: Icon(Icons.science), 162 | label: Text("Scientific"), 163 | ), 164 | const NavigationDrawerDestination( 165 | icon: Icon(Icons.date_range_outlined), 166 | selectedIcon: Icon(Icons.date_range), 167 | label: Text("Date"), 168 | ), 169 | Padding( 170 | padding: const EdgeInsets.fromLTRB(28, 16, 16, 10), 171 | child: Text( 172 | 'Converter', 173 | style: Theme.of(context).textTheme.titleSmall, 174 | ), 175 | ), 176 | const NavigationDrawerDestination( 177 | icon: Icon(Icons.architecture), 178 | selectedIcon: Icon(Icons.architecture), 179 | label: Text("Angle"), 180 | ), 181 | const NavigationDrawerDestination( 182 | icon: Icon(Icons.thermostat), 183 | selectedIcon: Icon(Icons.thermostat), 184 | label: Text("Temperature"), 185 | ), 186 | const NavigationDrawerDestination( 187 | icon: Icon(Icons.sd_card_outlined), 188 | selectedIcon: Icon(Icons.sd_card), 189 | label: Text("Data"), 190 | ), 191 | const NavigationDrawerDestination( 192 | icon: Icon(Icons.watch_later_outlined), 193 | selectedIcon: Icon(Icons.watch_later), 194 | label: Text("Time"), 195 | ), 196 | const NavigationDrawerDestination( 197 | icon: Icon(Icons.crop), 198 | selectedIcon: Icon(Icons.crop), 199 | label: Text("Area"), 200 | ), 201 | const NavigationDrawerDestination( 202 | icon: Icon(Icons.straighten), 203 | selectedIcon: Icon(Icons.straighten), 204 | label: Text("Length"), 205 | ), 206 | const NavigationDrawerDestination( 207 | icon: Icon(Icons.free_breakfast_outlined), 208 | selectedIcon: Icon(Icons.free_breakfast), 209 | label: Text("Volume"), 210 | ), 211 | const NavigationDrawerDestination( 212 | icon: Icon(Icons.scale_outlined), 213 | selectedIcon: Icon(Icons.scale), 214 | label: Text("Mass"), 215 | ), 216 | const NavigationDrawerDestination( 217 | icon: Icon(Icons.speed), 218 | selectedIcon: Icon(Icons.speed), 219 | label: Text("Pressure"), 220 | ), 221 | const NavigationDrawerDestination( 222 | icon: Icon(Icons.run_circle_outlined), 223 | selectedIcon: Icon(Icons.run_circle), 224 | label: Text("Speed"), 225 | ), 226 | ], 227 | ), 228 | ); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /lib/models/settings_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | class SettingsModel extends ChangeNotifier { 5 | bool _isSystemColor = false; 6 | ThemeMode _themeMode = ThemeMode.system; 7 | int _sigFig = 7; 8 | int _customColor = 16777215; 9 | bool _firstLaunch = true; 10 | 11 | bool get isSystemColor => _isSystemColor; 12 | set isSystemColor(bool value) { 13 | if (_isSystemColor == value) return; 14 | _isSystemColor = value; 15 | notifyListeners(); 16 | save(); 17 | } 18 | 19 | ThemeMode get themeMode => _themeMode; 20 | set themeMode(ThemeMode value) { 21 | if (_themeMode == value) return; 22 | _themeMode = value; 23 | notifyListeners(); 24 | save(); 25 | } 26 | 27 | int get sigFig => _sigFig; 28 | set sigFig(int value) { 29 | if (_sigFig == value) return; 30 | _sigFig = value; 31 | notifyListeners(); 32 | save(); 33 | } 34 | 35 | int get customColor => _customColor; 36 | set customColor(int value) { 37 | if (_customColor == value) return; 38 | _customColor = value; 39 | notifyListeners(); 40 | save(); 41 | } 42 | 43 | bool get firstLaunch => _firstLaunch; 44 | set firstLaunch(bool value) { 45 | if (_firstLaunch == value) return; 46 | _firstLaunch = value; 47 | notifyListeners(); 48 | save(); 49 | } 50 | 51 | Future save() async { 52 | final prefs = await SharedPreferences.getInstance(); 53 | prefs.setBool('isSystemColor', _isSystemColor); 54 | prefs.setString('themeMode', _themeMode.toString()); 55 | prefs.setInt('sigFig', _sigFig); 56 | prefs.setInt('customColor', _customColor); 57 | prefs.setBool('firstLaunch', _firstLaunch); 58 | } 59 | 60 | Future load() async { 61 | final prefs = await SharedPreferences.getInstance(); 62 | _isSystemColor = prefs.getBool('isSystemColor') ?? false; 63 | _themeMode = ThemeMode.values.firstWhere( 64 | (e) => e.toString() == prefs.getString('themeMode'), 65 | orElse: () => ThemeMode.system, 66 | ); 67 | _sigFig = prefs.getInt('sigFig') ?? 7; 68 | _customColor = prefs.getInt('customColor') ?? 16777215; 69 | _firstLaunch = prefs.getBool('firstLaunch') ?? true; 70 | notifyListeners(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/pages/calc/date_calc.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:intl/intl.dart'; 3 | 4 | class DateCalc extends StatefulWidget { 5 | const DateCalc({Key? key}) : super(key: key); 6 | static String pageTitle = "Date"; 7 | 8 | @override 9 | State createState() => _DateCalcState(); 10 | } 11 | 12 | class _DateCalcState extends State 13 | with AutomaticKeepAliveClientMixin { 14 | final List _pages = [const BuildDateDiff(), const AddSubdays()]; 15 | int selectedIndex = 0; 16 | @override 17 | Widget build(BuildContext context) { 18 | super.build(context); 19 | return DefaultTabController( 20 | length: _pages.length, 21 | child: Scaffold( 22 | resizeToAvoidBottomInset: false, 23 | appBar: const TabBar(tabs: [ 24 | Tab( 25 | text: "Date Difference", 26 | ), 27 | Tab( 28 | text: "Add/Subtract Days", 29 | ) 30 | ]), 31 | body: SafeArea(child: TabBarView(children: _pages)), 32 | ), 33 | ); 34 | } 35 | 36 | @override 37 | bool get wantKeepAlive => true; 38 | } 39 | 40 | class BuildDateDiff extends StatefulWidget { 41 | const BuildDateDiff({super.key}); 42 | 43 | @override 44 | State createState() => _BuildDateDiffState(); 45 | } 46 | 47 | class _BuildDateDiffState extends State 48 | with AutomaticKeepAliveClientMixin { 49 | DateTime selectedDate1 = DateTime.now(); 50 | DateTime selectedDate2 = DateTime.now(); 51 | Future _selectDate1(BuildContext context) async { 52 | final DateTime? picked = await showDatePicker( 53 | context: context, 54 | initialDate: selectedDate1, 55 | firstDate: DateTime(1600, 1, 1), 56 | lastDate: DateTime(2550, 12, 31), 57 | ); 58 | if (picked != null && picked != selectedDate1) { 59 | setState(() { 60 | selectedDate1 = picked; 61 | }); 62 | } 63 | } 64 | 65 | Future _selectDate2(BuildContext context) async { 66 | final DateTime? picked = await showDatePicker( 67 | context: context, 68 | initialDate: selectedDate2, 69 | firstDate: DateTime(1600, 1, 1), 70 | lastDate: DateTime(2550, 12, 31), 71 | ); 72 | if (picked != null && picked != selectedDate2) { 73 | setState(() { 74 | selectedDate2 = picked; 75 | }); 76 | } 77 | } 78 | 79 | int daysBetween(DateTime from, DateTime to) { 80 | from = DateTime(from.year, from.month, from.day); 81 | to = DateTime(to.year, to.month, to.day); 82 | return to.difference(from).inMilliseconds.abs(); 83 | } 84 | 85 | String formatMilliseconds(int milliseconds) { 86 | // Number of milliseconds in a day, week, month, and year 87 | const int millisecondsPerDay = 86400000; 88 | const int millisecondsPerWeek = 604800000; 89 | const int millisecondsPerMonth = 2629800000; 90 | const int millisecondsPerYear = 31557600000; 91 | 92 | int years = milliseconds ~/ millisecondsPerYear; 93 | int months = (milliseconds % millisecondsPerYear) ~/ millisecondsPerMonth; 94 | int weeks = ((milliseconds % millisecondsPerYear) % millisecondsPerMonth) ~/ 95 | millisecondsPerWeek; 96 | int days = (((milliseconds % millisecondsPerYear) % millisecondsPerMonth) % 97 | millisecondsPerWeek) ~/ 98 | millisecondsPerDay; 99 | 100 | String result = ''; 101 | if (years > 0) { 102 | result += '${years.toString()} ${years == 1 ? 'year' : 'years'}'; 103 | } 104 | if (months > 0) { 105 | result += 106 | '${result.isNotEmpty ? ', ' : ''}${months.toString()} ${months == 1 ? 'month' : 'months'}'; 107 | } 108 | if (weeks > 0) { 109 | result += 110 | '${result.isNotEmpty ? ', ' : ''}${weeks.toString()} ${weeks == 1 ? 'week' : 'weeks'}'; 111 | } 112 | if (days > 0) { 113 | result += 114 | '${result.isNotEmpty ? ', ' : ''}${days.toString()} ${days == 1 ? 'day' : 'days'}'; 115 | } 116 | 117 | return result; 118 | } 119 | 120 | @override 121 | Widget build(BuildContext context) { 122 | super.build(context); 123 | return Padding( 124 | padding: const EdgeInsets.all(16.0), 125 | child: SingleChildScrollView( 126 | child: Column( 127 | crossAxisAlignment: CrossAxisAlignment.start, 128 | mainAxisSize: MainAxisSize.min, 129 | children: [ 130 | const Text("From"), 131 | const SizedBox( 132 | height: 8, 133 | ), 134 | InkWell( 135 | child: Chip( 136 | label: Row( 137 | mainAxisSize: MainAxisSize.min, 138 | mainAxisAlignment: MainAxisAlignment.start, 139 | crossAxisAlignment: CrossAxisAlignment.center, 140 | children: [ 141 | const Icon(Icons.date_range_outlined), 142 | Text(DateFormat.yMMMd().format(selectedDate1).toString()), 143 | ], 144 | )), 145 | onTap: () => _selectDate1(context), 146 | ), 147 | const SizedBox( 148 | height: 36, 149 | ), 150 | const Text("To"), 151 | const SizedBox( 152 | height: 8, 153 | ), 154 | InkWell( 155 | child: Chip( 156 | label: Row( 157 | mainAxisAlignment: MainAxisAlignment.start, 158 | crossAxisAlignment: CrossAxisAlignment.center, 159 | mainAxisSize: MainAxisSize.min, 160 | children: [ 161 | const Icon(Icons.date_range_outlined), 162 | Text(DateFormat.yMMMd().format(selectedDate2).toString()), 163 | ], 164 | )), 165 | onTap: () => _selectDate2(context), 166 | ), 167 | const SizedBox( 168 | height: 36, 169 | ), 170 | const Text("Difference"), 171 | const SizedBox( 172 | height: 8, 173 | ), 174 | selectedDate1.difference(selectedDate2).inMilliseconds == 0 175 | ? const Text( 176 | "Same day", 177 | style: TextStyle(fontSize: 36), 178 | ) 179 | : Text( 180 | formatMilliseconds( 181 | daysBetween(selectedDate1, selectedDate2)), 182 | style: const TextStyle(fontSize: 36), 183 | ) 184 | ], 185 | ), 186 | ), 187 | ); 188 | } 189 | 190 | @override 191 | bool get wantKeepAlive => true; 192 | } 193 | 194 | class AddSubdays extends StatefulWidget { 195 | const AddSubdays({super.key}); 196 | 197 | @override 198 | State createState() => _AddSubdaysState(); 199 | } 200 | 201 | class _AddSubdaysState extends State 202 | with AutomaticKeepAliveClientMixin { 203 | DateTime fromDate = DateTime.now(); 204 | int newSelectedYear = 0; 205 | int newSelectedMonth = 0; 206 | int newSelectedDay = 0; 207 | 208 | Future _selectDate(BuildContext context) async { 209 | final DateTime? picked = await showDatePicker( 210 | context: context, 211 | initialDate: fromDate, 212 | firstDate: DateTime(1600, 1, 1), 213 | lastDate: DateTime(2550, 12, 31), 214 | ); 215 | if (picked != null && picked != fromDate) { 216 | setState(() { 217 | fromDate = picked; 218 | }); 219 | } 220 | } 221 | 222 | @override 223 | Widget build(BuildContext context) { 224 | super.build(context); 225 | return Padding( 226 | padding: const EdgeInsets.all(16.0), 227 | child: SingleChildScrollView( 228 | child: Column( 229 | crossAxisAlignment: CrossAxisAlignment.start, 230 | mainAxisSize: MainAxisSize.min, 231 | children: [ 232 | const Text("From"), 233 | const SizedBox( 234 | height: 8, 235 | ), 236 | InkWell( 237 | child: Chip( 238 | label: Row( 239 | mainAxisSize: MainAxisSize.min, 240 | mainAxisAlignment: MainAxisAlignment.start, 241 | crossAxisAlignment: CrossAxisAlignment.center, 242 | children: [ 243 | const Icon(Icons.date_range_outlined), 244 | Text(DateFormat.yMMMd().format(fromDate).toString()), 245 | ], 246 | )), 247 | onTap: () => _selectDate(context), 248 | ), 249 | const SizedBox( 250 | height: 36, 251 | ), 252 | GridView( 253 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 254 | crossAxisCount: 3), 255 | shrinkWrap: true, 256 | padding: EdgeInsets.zero, 257 | physics: const NeverScrollableScrollPhysics(), 258 | children: [ 259 | DropdownMenu( 260 | width: 96, 261 | dropdownMenuEntries: List.generate( 262 | 99, 263 | (index) => DropdownMenuEntry( 264 | style: ButtonStyle( 265 | fixedSize: MaterialStateProperty.all( 266 | const Size.fromWidth(96), 267 | ), 268 | ), 269 | value: index, 270 | label: index.toString(), 271 | ), 272 | growable: false), 273 | initialSelection: 0, 274 | label: const Text("Year"), 275 | onSelected: (value) => setState(() { 276 | newSelectedYear = value!; 277 | }), 278 | ), 279 | DropdownMenu( 280 | width: 96, 281 | dropdownMenuEntries: List.generate( 282 | 99, 283 | (index) => DropdownMenuEntry( 284 | style: ButtonStyle( 285 | fixedSize: MaterialStateProperty.all( 286 | const Size.fromWidth(96), 287 | ), 288 | ), 289 | value: index, 290 | label: index.toString(), 291 | ), 292 | growable: false), 293 | initialSelection: 0, 294 | label: const Text("Month"), 295 | onSelected: (value) => setState(() { 296 | newSelectedMonth = value!; 297 | }), 298 | ), 299 | DropdownMenu( 300 | width: 96, 301 | dropdownMenuEntries: List.generate( 302 | 99, 303 | (index) => DropdownMenuEntry( 304 | style: ButtonStyle( 305 | fixedSize: MaterialStateProperty.all( 306 | const Size.fromWidth(96), 307 | ), 308 | ), 309 | value: index, 310 | label: index.toString(), 311 | ), 312 | growable: false), 313 | initialSelection: 0, 314 | label: const Text("Day"), 315 | onSelected: (value) => setState(() { 316 | newSelectedDay = value!; 317 | }), 318 | ), 319 | ], 320 | ), 321 | GridView( 322 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 323 | crossAxisCount: 2), 324 | shrinkWrap: true, 325 | children: [ 326 | Column( 327 | mainAxisAlignment: MainAxisAlignment.start, 328 | crossAxisAlignment: CrossAxisAlignment.start, 329 | children: [ 330 | const Text("Adding:"), 331 | Text( 332 | DateFormat.yMMMd() 333 | .format(DateTime( 334 | fromDate.year + newSelectedYear, 335 | fromDate.month + newSelectedMonth, 336 | fromDate.day + newSelectedDay)) 337 | .toString(), 338 | style: const TextStyle(fontSize: 24), 339 | ), 340 | ], 341 | ), 342 | Column( 343 | mainAxisAlignment: MainAxisAlignment.start, 344 | crossAxisAlignment: CrossAxisAlignment.start, 345 | children: [ 346 | const Text("Subtracting:"), 347 | Text( 348 | DateFormat.yMMMd() 349 | .format(DateTime( 350 | fromDate.year - newSelectedYear, 351 | fromDate.month - newSelectedMonth, 352 | fromDate.day - newSelectedDay)) 353 | .toString(), 354 | style: const TextStyle(fontSize: 24), 355 | ), 356 | ], 357 | ) 358 | ], 359 | ) 360 | ], 361 | ), 362 | ), 363 | ); 364 | } 365 | 366 | @override 367 | bool get wantKeepAlive => true; 368 | } 369 | -------------------------------------------------------------------------------- /lib/pages/calc/scientific_calc.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SciCalc extends StatelessWidget { 4 | const SciCalc({Key? key}) : super(key: key); 5 | static String pageTitle = "Scientific"; 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return const Scaffold( 10 | body: Center( 11 | child: Text("Under construction"), 12 | ), 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/pages/calc/standard_calc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:math_expressions/math_expressions.dart'; 6 | import 'package:responsive_builder/responsive_builder.dart'; 7 | import 'package:shared_preferences/shared_preferences.dart'; 8 | 9 | class StdCalc extends StatefulWidget { 10 | const StdCalc({Key? key}) : super(key: key); 11 | static String pageTitle = "Standard"; 12 | 13 | @override 14 | State createState() => _StdCalcState(); 15 | } 16 | 17 | class _StdCalcState extends State { 18 | TextEditingController input = TextEditingController(); 19 | final ScrollController _inputScroll = ScrollController(); 20 | final FocusNode _inputFocus = FocusNode(); 21 | 22 | RegExp bracketsCheck = RegExp(r'(?<=\d)(?=\()|(?<=\))(?=\d)|(?<=\))(?=\()'); 23 | var output = ""; 24 | void addToHistory(input, output) async { 25 | final SharedPreferences prefs = await SharedPreferences.getInstance(); 26 | var historyData = { 27 | "datetime": DateTime.now().toString(), 28 | "input": input, 29 | "output": output, 30 | }; 31 | List _history = jsonDecode(prefs.getString("history_key") ?? "[]"); 32 | if (_history.length >= 20) { 33 | _history.removeAt(20); 34 | _history.add(historyData); 35 | } else { 36 | _history.add(historyData); 37 | } 38 | String history = jsonEncode(_history); 39 | await prefs.setString("history_key", history); 40 | } 41 | 42 | Future listHistory() async { 43 | final SharedPreferences prefs = await SharedPreferences.getInstance(); 44 | final List listHistory = jsonDecode(prefs.getString("history_key") ?? "[]"); 45 | 46 | return listHistory; 47 | } 48 | 49 | void scrollWithCursor(String val) { 50 | String blankText = ""; 51 | final isLong = val.length > blankText.length; 52 | if (isLong) { 53 | _inputScroll.animateTo(_inputScroll.position.maxScrollExtent, 54 | duration: const Duration(milliseconds: 300), curve: Curves.ease); 55 | } 56 | print(_inputScroll.position.maxScrollExtent); 57 | print(input.selection.extentOffset); 58 | } 59 | 60 | void _doMath(String val) { 61 | if (val == "=") { 62 | if (input.text.isNotEmpty) { 63 | var userinput = input.text 64 | .replaceAll("\u00d7", "*") 65 | .replaceAll("÷", "/") 66 | .replaceAll(bracketsCheck, "*"); 67 | Parser P = Parser(); 68 | try { 69 | Expression expression = P.parse(userinput); 70 | 71 | ContextModel cm = ContextModel(); 72 | var finalvalue = expression.evaluate(EvaluationType.REAL, cm); 73 | setState(() { 74 | output = finalvalue.toString(); 75 | }); 76 | if (output.endsWith(".0")) { 77 | setState(() { 78 | output = output.substring(0, output.length - 2); 79 | }); 80 | } 81 | addToHistory(userinput, output); 82 | input.clear(); 83 | input.value = TextEditingValue( 84 | text: input.text.replaceRange( 85 | input.selection.start.abs(), input.selection.end.abs(), output), 86 | selection: TextSelection.collapsed( 87 | offset: input.selection.baseOffset + output.length), 88 | ); 89 | } on Exception catch (e) { 90 | setState(() { 91 | output = "Syntax Error $e"; 92 | }); 93 | } 94 | } 95 | listHistory(); 96 | } else if (val == "()") { 97 | if (input.selection.isCollapsed) { 98 | setState(() { 99 | input.value = input.value 100 | .replaced(TextRange.collapsed(input.selection.baseOffset), val); 101 | }); 102 | } else { 103 | setState(() { 104 | input.value = input.value.replaced( 105 | TextRange(start: input.selection.start, end: input.selection.end), 106 | '(${input.text.substring(input.selection.start, input.selection.end)})'); 107 | }); 108 | input.selection = TextSelection.fromPosition( 109 | TextPosition(offset: input.selection.end - 1)); 110 | } 111 | } else { 112 | if (input.selection.isCollapsed) { 113 | setState(() { 114 | input.value = input.value 115 | .replaced(TextRange.collapsed(input.selection.baseOffset), val); 116 | }); 117 | } else { 118 | setState(() { 119 | input.value = input.value.replaced( 120 | TextRange(start: input.selection.start, end: input.selection.end), 121 | val); 122 | }); 123 | input.selection = TextSelection.fromPosition( 124 | TextPosition(offset: input.selection.end)); 125 | } 126 | } 127 | _inputScroll.animateTo(_inputScroll.position.maxScrollExtent + 1, 128 | duration: const Duration(milliseconds: 300), curve: Curves.ease); 129 | } 130 | 131 | void _bkspc() { 132 | if (input.text.isNotEmpty) { 133 | if (input.selection.isCollapsed) { 134 | if (input.selection.baseOffset == input.text.length) { 135 | setState(() { 136 | input.value = TextEditingValue( 137 | text: input.text.substring(0, input.text.length - 1)); 138 | }); 139 | input.selection = TextSelection.fromPosition( 140 | TextPosition(offset: input.text.length)); 141 | } else { 142 | setState(() { 143 | input.value = input.value.replaced( 144 | TextRange( 145 | start: input.selection.baseOffset - 1, 146 | end: input.selection.baseOffset), 147 | ""); 148 | }); 149 | input.selection = TextSelection.fromPosition( 150 | TextPosition(offset: input.selection.start)); 151 | } 152 | } else { 153 | setState(() { 154 | input.value = input.value.replaced( 155 | TextRange(start: input.selection.start, end: input.selection.end), 156 | ""); 157 | }); 158 | input.selection = TextSelection.fromPosition( 159 | TextPosition(offset: input.selection.end)); 160 | } 161 | } else { 162 | setState(() { 163 | input.clear(); 164 | output = input.text; 165 | }); 166 | } 167 | } 168 | 169 | @override 170 | void initState() { 171 | super.initState(); 172 | setState(() { 173 | input.value = 174 | const TextEditingValue(selection: TextSelection.collapsed(offset: 0)); 175 | }); 176 | } 177 | 178 | @override 179 | void dispose() { 180 | super.dispose(); 181 | input.dispose(); 182 | _inputScroll.dispose(); 183 | } 184 | 185 | @override 186 | Widget build(BuildContext context) { 187 | return Scaffold( 188 | resizeToAvoidBottomInset: false, 189 | body: SafeArea( 190 | child: ResponsiveBuilder( 191 | builder: (context, sizingInformation) { 192 | if (sizingInformation.deviceScreenType == DeviceScreenType.tablet) { 193 | return OrientationBuilder( 194 | builder: (context, orientation) { 195 | if (orientation == Orientation.landscape) { 196 | return Row( 197 | crossAxisAlignment: CrossAxisAlignment.center, 198 | mainAxisAlignment: MainAxisAlignment.center, 199 | children: [ 200 | Expanded( 201 | child: Column( 202 | children: [ 203 | Expanded( 204 | child: _inputView(context), 205 | ), 206 | Expanded( 207 | child: _history(context, false), 208 | ), 209 | ], 210 | ), 211 | ), 212 | Expanded(child: _keypad(context, 1.046)) 213 | ], 214 | ); 215 | } else { 216 | return Column( 217 | crossAxisAlignment: CrossAxisAlignment.center, 218 | mainAxisAlignment: MainAxisAlignment.center, 219 | children: [ 220 | Expanded( 221 | child: Row( 222 | children: [ 223 | Expanded( 224 | child: _history(context, false), 225 | ), 226 | Expanded( 227 | child: _inputView(context), 228 | ), 229 | ], 230 | ), 231 | ), 232 | _keypad(context, 2) 233 | ], 234 | ); 235 | } 236 | }, 237 | ); 238 | } 239 | return OrientationBuilder( 240 | builder: (context, orientation) { 241 | if (orientation == Orientation.landscape) { 242 | return Row( 243 | children: [ 244 | Expanded(child: _inputView(context)), 245 | Expanded(child: _keypad(context, 1.8)), 246 | ], 247 | ); 248 | } else { 249 | return Column( 250 | children: [ 251 | Expanded( 252 | flex: 1, 253 | child: _inputView(context), 254 | ), 255 | _keypad(context, (1 / 1)) 256 | ], 257 | ); 258 | } 259 | }, 260 | ); 261 | }, 262 | ), 263 | ), 264 | ); 265 | } 266 | 267 | Widget _inputView(BuildContext context) { 268 | return Stack( 269 | children: [ 270 | Container( 271 | decoration: BoxDecoration( 272 | borderRadius: BorderRadius.circular(16), 273 | color: Theme.of(context).colorScheme.secondaryContainer, 274 | ), 275 | padding: const EdgeInsets.all(8), 276 | margin: const EdgeInsets.all(8), 277 | child: Column( 278 | crossAxisAlignment: CrossAxisAlignment.end, 279 | mainAxisAlignment: MainAxisAlignment.end, 280 | children: [ 281 | TextField( 282 | enableSuggestions: false, 283 | autofocus: true, 284 | textAlign: TextAlign.right, 285 | decoration: const InputDecoration(border: InputBorder.none), 286 | controller: input, 287 | focusNode: _inputFocus, 288 | scrollController: _inputScroll, 289 | inputFormatters: [ 290 | FilteringTextInputFormatter.deny(RegExp(r'[a-z] [A-Z] :$')) 291 | ], 292 | style: const TextStyle( 293 | fontSize: 48, 294 | ), 295 | keyboardType: TextInputType.none, 296 | ), 297 | const Divider(), 298 | const SizedBox( 299 | height: 10, 300 | ), 301 | Text( 302 | output.toString(), 303 | style: const TextStyle( 304 | fontSize: 30, 305 | ), 306 | ), 307 | ], 308 | ), 309 | ), 310 | getDeviceType(Size(MediaQuery.of(context).size.width, 311 | MediaQuery.of(context).size.height)) == 312 | DeviceScreenType.tablet 313 | ? const SizedBox.shrink() 314 | : Padding( 315 | padding: const EdgeInsets.all(16.0), 316 | child: IconButton( 317 | onPressed: () { 318 | showDialog( 319 | context: context, 320 | builder: (context) => Dialog.fullscreen( 321 | child: Column( 322 | children: [ 323 | AppBar( 324 | leading: IconButton( 325 | onPressed: () => Navigator.pop(context), 326 | icon: const Icon(Icons.close)), 327 | ), 328 | Expanded(child: _history(context, true)), 329 | ], 330 | )), 331 | ); 332 | }, 333 | icon: const Icon(Icons.history)), 334 | ), 335 | ], 336 | ); 337 | } 338 | 339 | Widget _keypad(BuildContext context, double cellSizeRatio) { 340 | return GridView( 341 | padding: const EdgeInsets.all(8), 342 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 343 | crossAxisCount: 4, 344 | crossAxisSpacing: 8, 345 | mainAxisSpacing: 8, 346 | childAspectRatio: cellSizeRatio), 347 | shrinkWrap: true, 348 | physics: const NeverScrollableScrollPhysics(), 349 | children: [ 350 | FilledButton( 351 | onPressed: () { 352 | setState(() { 353 | input.clear(); 354 | output = input.text; 355 | HapticFeedback.lightImpact(); 356 | }); 357 | }, 358 | child: const Text( 359 | "C", 360 | style: TextStyle(fontSize: 32), 361 | )), 362 | _buildCalcButton("()", true), 363 | _buildCalcButton("%", true), 364 | _buildCalcButton("÷", true), 365 | _buildCalcButton("7", false), 366 | _buildCalcButton("8", false), 367 | _buildCalcButton("9", false), 368 | _buildCalcButton("\u00d7", true), 369 | _buildCalcButton("4", false), 370 | _buildCalcButton("5", false), 371 | _buildCalcButton("6", false), 372 | _buildCalcButton("\u2013", true), 373 | _buildCalcButton("1", false), 374 | _buildCalcButton("2", false), 375 | _buildCalcButton("3", false), 376 | _buildCalcButton("+", true), 377 | _buildCalcButton(".", false), 378 | _buildCalcButton("0", false), 379 | FilledButton.tonal( 380 | onPressed: () { 381 | _bkspc(); 382 | 383 | HapticFeedback.lightImpact(); 384 | }, 385 | child: const Icon( 386 | Icons.backspace_outlined, 387 | size: 32, 388 | )), 389 | _buildCalcButton("=", true), 390 | ], 391 | ); 392 | } 393 | 394 | Widget _history(BuildContext context, bool isPhone) { 395 | bool isPhone = true; 396 | return Container( 397 | decoration: BoxDecoration( 398 | borderRadius: BorderRadius.circular(16), 399 | color: Theme.of(context).colorScheme.secondaryContainer, 400 | ), 401 | padding: const EdgeInsets.all(8), 402 | margin: const EdgeInsets.all(8), 403 | child: FutureBuilder( 404 | future: listHistory(), 405 | builder: (context, snapshot) { 406 | if (!snapshot.hasData) { 407 | return const Center( 408 | child: Text("History is Empty"), 409 | ); 410 | } 411 | return ListView.builder( 412 | itemCount: snapshot.data?.length, 413 | itemBuilder: (context, index) { 414 | var math = snapshot.data?[index]; 415 | return ListTile( 416 | title: InkWell( 417 | onTap: () { 418 | input.clear(); 419 | 420 | setState(() { 421 | output = ""; 422 | input.value = input.value.replaced( 423 | TextRange.collapsed(input.selection.baseOffset), 424 | math["input"]); 425 | }); 426 | if (isPhone) { 427 | Navigator.pop(context); 428 | } 429 | }, 430 | child: Text( 431 | math["input"], 432 | style: Theme.of(context).textTheme.headlineLarge, 433 | ), 434 | ), 435 | subtitle: Text( 436 | math["output"], 437 | style: Theme.of(context).textTheme.headlineMedium, 438 | ), 439 | ); 440 | }, 441 | ); 442 | }), 443 | ); 444 | } 445 | 446 | Widget _buildCalcButton(String val, bool notTonalButton) { 447 | String valb; 448 | if (val == "\u2013") { 449 | valb = "-"; 450 | } else if (val == "÷") { 451 | valb = "/"; 452 | } else { 453 | valb = val; 454 | } 455 | return notTonalButton 456 | ? FilledButton( 457 | onPressed: () { 458 | HapticFeedback.lightImpact(); 459 | _doMath(valb); 460 | }, 461 | child: Text( 462 | val, 463 | style: const TextStyle( 464 | fontSize: 32, 465 | ), 466 | ), 467 | ) 468 | : FilledButton.tonal( 469 | onPressed: () { 470 | HapticFeedback.lightImpact(); 471 | _doMath(valb); 472 | }, 473 | child: Text( 474 | val, 475 | style: const TextStyle( 476 | fontSize: 32, 477 | ), 478 | ), 479 | ); 480 | } 481 | } 482 | -------------------------------------------------------------------------------- /lib/pages/conv/area_conv.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:responsive_builder/responsive_builder.dart'; 7 | import 'package:units_converter/units_converter.dart'; 8 | 9 | import '../../models/settings_model.dart'; 10 | import '../settings_page.dart'; 11 | 12 | class AreaConv extends StatefulWidget { 13 | const AreaConv({Key? key}) : super(key: key); 14 | 15 | static String pageTitle = "Area"; 16 | @override 17 | State createState() => _AreaConvState(); 18 | } 19 | 20 | class _AreaConvState extends State { 21 | TextEditingController inputA = TextEditingController(); 22 | FocusNode inputAFN = FocusNode(); 23 | TextEditingController inputB = TextEditingController(); 24 | FocusNode inputBFN = FocusNode(); 25 | 26 | var selectedareaA; 27 | var selectedareaB; 28 | var selectedareaSymbolA; 29 | var selectedareaSymbolB; 30 | var area = Area(significantFigures: 7, removeTrailingZeros: true); 31 | var units = []; 32 | 33 | void _bkspc() { 34 | if (inputAFN.hasFocus) { 35 | if (inputA.text.isNotEmpty) { 36 | if (inputA.selection.isCollapsed) { 37 | if (inputA.selection.baseOffset == inputA.text.length) { 38 | setState(() { 39 | inputA.text = inputA.text.substring(0, inputA.text.length - 1); 40 | }); 41 | inputA.selection = TextSelection.fromPosition( 42 | TextPosition(offset: inputA.text.length)); 43 | } else { 44 | setState(() { 45 | inputA.value = inputA.value.replaced( 46 | TextRange( 47 | start: inputA.selection.baseOffset - 1, 48 | end: inputA.selection.baseOffset), 49 | ""); 50 | }); 51 | inputA.selection = TextSelection.fromPosition( 52 | TextPosition(offset: inputA.selection.start)); 53 | } 54 | } else { 55 | setState(() { 56 | inputA.value = inputA.value.replaced( 57 | TextRange( 58 | start: inputA.selection.start, end: inputA.selection.end), 59 | ""); 60 | }); 61 | inputA.selection = TextSelection.fromPosition( 62 | TextPosition(offset: inputA.selection.end)); 63 | } 64 | if (inputA.text.isNotEmpty) { 65 | area.convert(selectedareaA, double.parse(inputA.text)); 66 | 67 | units = area.getAll(); 68 | 69 | _convValueBuild(units); 70 | inputB.text = unitDetails[selectedareaB] ?? ""; 71 | } else { 72 | setState(() { 73 | inputA.clear(); 74 | inputB.clear(); 75 | }); 76 | } 77 | } 78 | } else if (inputBFN.hasFocus) { 79 | if (inputB.text.isNotEmpty) { 80 | if (inputB.selection.isCollapsed) { 81 | if (inputB.selection.baseOffset == inputB.text.length) { 82 | setState(() { 83 | inputB.text = inputB.text.substring(0, inputB.text.length - 1); 84 | }); 85 | inputB.selection = TextSelection.fromPosition( 86 | TextPosition(offset: inputB.text.length)); 87 | } else { 88 | setState(() { 89 | inputB.value = inputB.value.replaced( 90 | TextRange( 91 | start: inputB.selection.baseOffset - 1, 92 | end: inputB.selection.baseOffset), 93 | ""); 94 | }); 95 | inputB.selection = TextSelection.fromPosition( 96 | TextPosition(offset: inputB.selection.start)); 97 | } 98 | } else { 99 | setState(() { 100 | inputB.value = inputB.value.replaced( 101 | TextRange( 102 | start: inputB.selection.start, end: inputB.selection.end), 103 | ""); 104 | }); 105 | inputB.selection = TextSelection.fromPosition( 106 | TextPosition(offset: inputB.selection.end)); 107 | } 108 | if (inputB.text.isNotEmpty) { 109 | area.convert(selectedareaB, double.parse(inputB.text)); 110 | units = area.getAll(); 111 | 112 | _convValueBuild(units); 113 | inputA.text = unitDetails[selectedareaA] ?? ""; 114 | } else { 115 | setState(() { 116 | inputA.clear(); 117 | inputB.clear(); 118 | }); 119 | } 120 | } 121 | } 122 | } 123 | 124 | Map unitDetails = {}; 125 | 126 | void _convValueBuild(unitsconv) { 127 | for (Unit unit in unitsconv) { 128 | if (unit.name == selectedareaA) { 129 | setState(() { 130 | selectedareaSymbolA = unit.symbol; 131 | }); 132 | } else if (unit.name == selectedareaB) { 133 | setState(() { 134 | selectedareaSymbolB = unit.symbol; 135 | }); 136 | } 137 | unitDetails.addAll({unit.name: unit.stringValue ?? ""}); 138 | } 139 | } 140 | 141 | void _conv(selectedUnit, val, TextEditingController input) { 142 | area.convert(selectedUnit, val); 143 | 144 | units = area.getAll(); 145 | 146 | _convValueBuild(units); 147 | input.text = unitDetails[selectedUnit] ?? ""; 148 | } 149 | 150 | void _convFunc(val) { 151 | if (val == "C") { 152 | inputA.clear(); 153 | inputB.clear(); 154 | } else { 155 | setState(() { 156 | if (inputAFN.hasFocus) { 157 | inputA.value = TextEditingValue( 158 | text: inputA.text.replaceRange( 159 | inputA.selection.start, inputA.selection.end, val), 160 | selection: TextSelection.collapsed( 161 | offset: inputA.selection.baseOffset + val.toString().length), 162 | ); 163 | area.convert(selectedareaA, double.parse(inputA.text)); 164 | 165 | units = area.getAll(); 166 | 167 | _convValueBuild(units); 168 | inputB.text = unitDetails[selectedareaB] ?? ""; 169 | } else if (inputBFN.hasFocus) { 170 | inputB.value = TextEditingValue( 171 | text: inputB.text.replaceRange( 172 | inputB.selection.start, inputB.selection.end, val), 173 | selection: TextSelection.collapsed( 174 | offset: inputB.selection.baseOffset + val.toString().length), 175 | ); 176 | area.convert(selectedareaB, double.parse(inputB.text)); 177 | 178 | units = area.getAll(); 179 | 180 | _convValueBuild(units); 181 | inputA.text = unitDetails[selectedareaA] ?? ""; 182 | } 183 | }); 184 | } 185 | } 186 | 187 | @override 188 | void initState() { 189 | super.initState(); 190 | setState(() { 191 | units = area.getAll(); 192 | selectedareaA = AREA.acres; 193 | selectedareaB = AREA.squareMeters; 194 | }); 195 | inputAFN.requestFocus(); 196 | _convValueBuild(units); 197 | } 198 | 199 | @override 200 | Widget build(BuildContext context) { 201 | SettingsModel settings = Provider.of(context); 202 | area.significantFigures = settings.sigFig; 203 | return Scaffold( 204 | resizeToAvoidBottomInset: false, 205 | body: SafeArea( 206 | child: ResponsiveBuilder( 207 | builder: (context, sizingInformation) { 208 | if (sizingInformation.deviceScreenType == DeviceScreenType.tablet) { 209 | return OrientationBuilder( 210 | builder: (context, orientation) { 211 | if (orientation == Orientation.landscape) { 212 | return Row( 213 | crossAxisAlignment: CrossAxisAlignment.center, 214 | mainAxisAlignment: MainAxisAlignment.center, 215 | children: [ 216 | Expanded( 217 | child: _inputView(context, 48), 218 | ), 219 | Expanded(child: _keypad(context, 1.42)) 220 | ], 221 | ); 222 | } else { 223 | return Column( 224 | crossAxisAlignment: CrossAxisAlignment.center, 225 | mainAxisAlignment: MainAxisAlignment.center, 226 | children: [ 227 | Expanded( 228 | child: _inputView(context, 48), 229 | ), 230 | _keypad(context, 2) 231 | ], 232 | ); 233 | } 234 | }, 235 | ); 236 | } 237 | return OrientationBuilder( 238 | builder: (context, orientation) { 239 | if (orientation == Orientation.landscape) { 240 | return Row( 241 | children: [ 242 | Expanded(child: _inputView(context, 32)), 243 | Expanded(child: _keypad(context, 2.4)), 244 | ], 245 | ); 246 | } else { 247 | return Column( 248 | children: [ 249 | Expanded( 250 | flex: 1, 251 | child: _inputView(context, 48), 252 | ), 253 | _keypad(context, 1.8) 254 | ], 255 | ); 256 | } 257 | }, 258 | ); 259 | }, 260 | ), 261 | ), 262 | ); 263 | } 264 | 265 | Widget _keypad(BuildContext context, double cellSizeRatio) { 266 | return GridView( 267 | shrinkWrap: true, 268 | padding: const EdgeInsets.all(8), 269 | physics: const NeverScrollableScrollPhysics(), 270 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 271 | crossAxisCount: 3, 272 | crossAxisSpacing: 8, 273 | mainAxisSpacing: 8, 274 | childAspectRatio: cellSizeRatio), 275 | children: [ 276 | FilledButton( 277 | onPressed: () { 278 | if (inputBFN.hasFocus) { 279 | inputBFN.unfocus(); 280 | inputAFN.requestFocus(); 281 | } else if (inputAFN.hasFocus) { 282 | inputAFN.unfocus(); 283 | inputBFN.requestFocus(); 284 | } 285 | }, 286 | child: Transform.rotate( 287 | angle: 90 * pi / 180, 288 | child: const Icon( 289 | Icons.compare_arrows, 290 | size: 32, 291 | ), 292 | )), 293 | _buildButtons("C", false), 294 | FilledButton( 295 | onPressed: () { 296 | _bkspc(); 297 | 298 | HapticFeedback.lightImpact(); 299 | }, 300 | child: const Icon( 301 | Icons.backspace_outlined, 302 | size: 32, 303 | )), 304 | _buildButtons("7", true), 305 | _buildButtons("8", true), 306 | _buildButtons("9", true), 307 | _buildButtons("4", true), 308 | _buildButtons("5", true), 309 | _buildButtons("6", true), 310 | _buildButtons("1", true), 311 | _buildButtons("2", true), 312 | _buildButtons("3", true), 313 | const FilledButton.tonal( 314 | onPressed: null, 315 | child: Text( 316 | "\u00b1", 317 | style: TextStyle( 318 | fontSize: 32, 319 | ), 320 | )), 321 | _buildButtons("0", true), 322 | _buildButtons(".", true), 323 | ], 324 | ); 325 | } 326 | 327 | Widget _inputView(BuildContext context, double fontsize) { 328 | return Container( 329 | decoration: BoxDecoration( 330 | borderRadius: BorderRadius.circular(16), 331 | color: Theme.of(context).colorScheme.secondaryContainer, 332 | ), 333 | padding: const EdgeInsets.all(8), 334 | margin: const EdgeInsets.all(8), 335 | child: Column( 336 | crossAxisAlignment: CrossAxisAlignment.start, 337 | mainAxisAlignment: MainAxisAlignment.start, 338 | children: [ 339 | DropdownMenu( 340 | dropdownMenuEntries: List.generate( 341 | units.length, 342 | growable: false, 343 | (index) { 344 | var unit = units[index]; 345 | return DropdownMenuEntry( 346 | value: unit.name, 347 | label: unit.name.toString().split("AREA.").last.capitalize(), 348 | ); 349 | }, 350 | ), 351 | initialSelection: selectedareaA, 352 | onSelected: (value) { 353 | setState(() { 354 | selectedareaA = value; 355 | }); 356 | if (inputAFN.hasFocus) { 357 | if (inputA.text.isNotEmpty) { 358 | area.convert(value, double.parse(inputA.text)); 359 | units = area.getAll(); 360 | 361 | _convValueBuild(units); 362 | inputB.text = unitDetails[selectedareaB] ?? ""; 363 | } 364 | } else if (inputBFN.hasFocus) { 365 | if (inputB.text.isNotEmpty) { 366 | area.convert(selectedareaB, double.parse(inputB.text)); 367 | units = area.getAll(); 368 | 369 | _convValueBuild(units); 370 | inputA.text = unitDetails[value] ?? ""; 371 | } 372 | } 373 | }, 374 | ), 375 | TextField( 376 | enableSuggestions: false, 377 | textAlign: TextAlign.right, 378 | decoration: InputDecoration( 379 | border: InputBorder.none, 380 | suffixText: selectedareaSymbolA.toString(), 381 | ), 382 | controller: inputA, 383 | focusNode: inputAFN, 384 | onChanged: (value) { 385 | if (inputAFN.hasFocus) { 386 | _conv(selectedareaA, value, inputB); 387 | } 388 | }, 389 | inputFormatters: [ 390 | FilteringTextInputFormatter.deny(RegExp(r'[a-z] [A-Z] :$')) 391 | ], 392 | style: TextStyle( 393 | fontSize: fontsize, 394 | ), 395 | keyboardType: TextInputType.none, 396 | ), 397 | const Divider(), 398 | DropdownMenu( 399 | dropdownMenuEntries: List.generate( 400 | units.length, 401 | growable: false, 402 | (index) { 403 | var unit = units[index]; 404 | return DropdownMenuEntry( 405 | value: unit.name, 406 | label: unit.name.toString().split("AREA.").last.capitalize(), 407 | ); 408 | }, 409 | ), 410 | initialSelection: selectedareaB, 411 | onSelected: (value) { 412 | setState(() { 413 | selectedareaB = value; 414 | }); 415 | if (inputBFN.hasFocus) { 416 | if (inputB.text.isNotEmpty) { 417 | area.convert(value, double.parse(inputB.text)); 418 | units = area.getAll(); 419 | 420 | _convValueBuild(units); 421 | inputA.text = unitDetails[selectedareaA] ?? ""; 422 | } 423 | } else if (inputAFN.hasFocus) { 424 | if (inputA.text.isNotEmpty) { 425 | area.convert(selectedareaA, double.parse(inputA.text)); 426 | units = area.getAll(); 427 | 428 | _convValueBuild(units); 429 | inputB.text = unitDetails[value] ?? ""; 430 | } 431 | } 432 | }, 433 | ), 434 | TextField( 435 | enableSuggestions: false, 436 | textAlign: TextAlign.right, 437 | decoration: InputDecoration( 438 | border: InputBorder.none, 439 | suffixText: selectedareaSymbolB.toString(), 440 | ), 441 | controller: inputB, 442 | focusNode: inputBFN, 443 | onChanged: (value) { 444 | if (inputBFN.hasFocus) { 445 | _conv(selectedareaB, value, inputA); 446 | } 447 | }, 448 | inputFormatters: [ 449 | FilteringTextInputFormatter.deny(RegExp(r'[a-z] [A-Z] :$')) 450 | ], 451 | style: TextStyle( 452 | fontSize: fontsize, 453 | ), 454 | keyboardType: TextInputType.none, 455 | ), 456 | ], 457 | ), 458 | ); 459 | } 460 | 461 | Widget _buildButtons(String label, bool tonal) { 462 | return tonal 463 | ? SizedBox( 464 | height: 32, 465 | width: 72, 466 | child: FilledButton.tonal( 467 | onPressed: () { 468 | _convFunc(label); 469 | HapticFeedback.lightImpact(); 470 | }, 471 | child: Text( 472 | label, 473 | style: const TextStyle( 474 | fontSize: 32, 475 | ), 476 | )), 477 | ) 478 | : SizedBox( 479 | height: 32, 480 | width: 72, 481 | child: FilledButton( 482 | onPressed: () { 483 | _convFunc(label); 484 | HapticFeedback.lightImpact(); 485 | }, 486 | child: Text( 487 | label, 488 | style: const TextStyle( 489 | fontSize: 32, 490 | ), 491 | )), 492 | ); 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /lib/pages/conv/energy_conv.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:responsive_builder/responsive_builder.dart'; 7 | import 'package:units_converter/units_converter.dart'; 8 | 9 | import '../../models/settings_model.dart'; 10 | import '../settings_page.dart'; 11 | 12 | class EnergyConv extends StatefulWidget { 13 | const EnergyConv({Key? key}) : super(key: key); 14 | static String pageTitle = "Energy"; 15 | 16 | @override 17 | State createState() => _EnergyConvState(); 18 | } 19 | 20 | class _EnergyConvState extends State { 21 | TextEditingController inputA = TextEditingController(); 22 | FocusNode inputAFN = FocusNode(); 23 | TextEditingController inputB = TextEditingController(); 24 | FocusNode inputBFN = FocusNode(); 25 | 26 | var selectedenergyA; 27 | var selectedenergyB; 28 | var selectedenergySymbolA; 29 | var selectedenergySymbolB; 30 | var energy = Energy(significantFigures: 7, removeTrailingZeros: true); 31 | var units = []; 32 | 33 | void _bkspc() { 34 | if (inputAFN.hasFocus) { 35 | if (inputA.text.isNotEmpty) { 36 | if (inputA.selection.isCollapsed) { 37 | if (inputA.selection.baseOffset == inputA.text.length) { 38 | setState(() { 39 | inputA.text = inputA.text.substring(0, inputA.text.length - 1); 40 | }); 41 | inputA.selection = TextSelection.fromPosition( 42 | TextPosition(offset: inputA.text.length)); 43 | } else { 44 | setState(() { 45 | inputA.value = inputA.value.replaced( 46 | TextRange( 47 | start: inputA.selection.baseOffset - 1, 48 | end: inputA.selection.baseOffset), 49 | ""); 50 | }); 51 | inputA.selection = TextSelection.fromPosition( 52 | TextPosition(offset: inputA.selection.start)); 53 | } 54 | } else { 55 | setState(() { 56 | inputA.value = inputA.value.replaced( 57 | TextRange( 58 | start: inputA.selection.start, end: inputA.selection.end), 59 | ""); 60 | }); 61 | inputA.selection = TextSelection.fromPosition( 62 | TextPosition(offset: inputA.selection.end)); 63 | } 64 | if (inputA.text.isNotEmpty) { 65 | energy.convert(selectedenergyA, double.parse(inputA.text)); 66 | 67 | units = energy.getAll(); 68 | 69 | _convValueBuild(units); 70 | inputB.text = unitDetails[selectedenergyB] ?? ""; 71 | } else { 72 | setState(() { 73 | inputA.clear(); 74 | inputB.clear(); 75 | }); 76 | } 77 | } 78 | } else if (inputBFN.hasFocus) { 79 | if (inputB.text.isNotEmpty) { 80 | if (inputB.selection.isCollapsed) { 81 | if (inputB.selection.baseOffset == inputB.text.length) { 82 | setState(() { 83 | inputB.text = inputB.text.substring(0, inputB.text.length - 1); 84 | }); 85 | inputB.selection = TextSelection.fromPosition( 86 | TextPosition(offset: inputB.text.length)); 87 | } else { 88 | setState(() { 89 | inputB.value = inputB.value.replaced( 90 | TextRange( 91 | start: inputB.selection.baseOffset - 1, 92 | end: inputB.selection.baseOffset), 93 | ""); 94 | }); 95 | inputB.selection = TextSelection.fromPosition( 96 | TextPosition(offset: inputB.selection.start)); 97 | } 98 | } else { 99 | setState(() { 100 | inputB.value = inputB.value.replaced( 101 | TextRange( 102 | start: inputB.selection.start, end: inputB.selection.end), 103 | ""); 104 | }); 105 | inputB.selection = TextSelection.fromPosition( 106 | TextPosition(offset: inputB.selection.end)); 107 | } 108 | if (inputB.text.isNotEmpty) { 109 | energy.convert(selectedenergyB, double.parse(inputB.text)); 110 | units = energy.getAll(); 111 | 112 | _convValueBuild(units); 113 | inputA.text = unitDetails[selectedenergyA] ?? ""; 114 | } else { 115 | setState(() { 116 | inputA.clear(); 117 | inputB.clear(); 118 | }); 119 | } 120 | } 121 | } 122 | } 123 | 124 | Map unitDetails = {}; 125 | 126 | void _convValueBuild(unitsconv) { 127 | for (Unit unit in unitsconv) { 128 | if (unit.name == selectedenergyA) { 129 | setState(() { 130 | selectedenergySymbolA = unit.symbol; 131 | }); 132 | } else if (unit.name == selectedenergyB) { 133 | setState(() { 134 | selectedenergySymbolB = unit.symbol; 135 | }); 136 | } 137 | unitDetails.addAll({unit.name: unit.stringValue ?? ""}); 138 | } 139 | } 140 | 141 | void _conv(selectedUnit, val, TextEditingController input) { 142 | energy.convert(selectedUnit, val); 143 | 144 | units = energy.getAll(); 145 | 146 | _convValueBuild(units); 147 | input.text = unitDetails[selectedUnit] ?? ""; 148 | } 149 | 150 | void _convFunc(val) { 151 | if (val == "C") { 152 | inputA.clear(); 153 | inputB.clear(); 154 | } else { 155 | setState(() { 156 | if (inputAFN.hasFocus) { 157 | inputA.value = TextEditingValue( 158 | text: inputA.text.replaceRange( 159 | inputA.selection.start, inputA.selection.end, val), 160 | selection: TextSelection.collapsed( 161 | offset: inputA.selection.baseOffset + val.toString().length), 162 | ); 163 | energy.convert(selectedenergyA, double.parse(inputA.text)); 164 | 165 | units = energy.getAll(); 166 | 167 | _convValueBuild(units); 168 | inputB.text = unitDetails[selectedenergyB] ?? ""; 169 | } else if (inputBFN.hasFocus) { 170 | inputB.value = TextEditingValue( 171 | text: inputB.text.replaceRange( 172 | inputB.selection.start, inputB.selection.end, val), 173 | selection: TextSelection.collapsed( 174 | offset: inputB.selection.baseOffset + val.toString().length), 175 | ); 176 | energy.convert(selectedenergyB, double.parse(inputB.text)); 177 | 178 | units = energy.getAll(); 179 | 180 | _convValueBuild(units); 181 | inputA.text = unitDetails[selectedenergyA] ?? ""; 182 | } 183 | }); 184 | } 185 | } 186 | 187 | @override 188 | void initState() { 189 | super.initState(); 190 | setState(() { 191 | units = energy.getAll(); 192 | selectedenergyA = ENERGY.calories; 193 | selectedenergyB = ENERGY.joules; 194 | }); 195 | inputAFN.requestFocus(); 196 | _convValueBuild(units); 197 | } 198 | 199 | @override 200 | Widget build(BuildContext context) { 201 | SettingsModel settings = Provider.of(context); 202 | energy.significantFigures = settings.sigFig; 203 | return Scaffold( 204 | resizeToAvoidBottomInset: false, 205 | body: SafeArea( 206 | child: ResponsiveBuilder( 207 | builder: (context, sizingInformation) { 208 | if (sizingInformation.deviceScreenType == DeviceScreenType.tablet) { 209 | return OrientationBuilder( 210 | builder: (context, orientation) { 211 | if (orientation == Orientation.landscape) { 212 | return Row( 213 | crossAxisAlignment: CrossAxisAlignment.center, 214 | mainAxisAlignment: MainAxisAlignment.center, 215 | children: [ 216 | Expanded( 217 | child: _inputView(context, 48), 218 | ), 219 | Expanded(child: _keypad(context, 1.42)) 220 | ], 221 | ); 222 | } else { 223 | return Column( 224 | crossAxisAlignment: CrossAxisAlignment.center, 225 | mainAxisAlignment: MainAxisAlignment.center, 226 | children: [ 227 | Expanded( 228 | child: _inputView(context, 48), 229 | ), 230 | _keypad(context, 2) 231 | ], 232 | ); 233 | } 234 | }, 235 | ); 236 | } 237 | return OrientationBuilder( 238 | builder: (context, orientation) { 239 | if (orientation == Orientation.landscape) { 240 | return Row( 241 | children: [ 242 | Expanded(child: _inputView(context, 32)), 243 | Expanded(child: _keypad(context, 2.4)), 244 | ], 245 | ); 246 | } else { 247 | return Column( 248 | children: [ 249 | Expanded( 250 | flex: 1, 251 | child: _inputView(context, 48), 252 | ), 253 | _keypad(context, 1.8) 254 | ], 255 | ); 256 | } 257 | }, 258 | ); 259 | }, 260 | ), 261 | ), 262 | ); 263 | } 264 | 265 | Widget _keypad(BuildContext context, double cellSizeRatio) { 266 | return GridView( 267 | shrinkWrap: true, 268 | padding: const EdgeInsets.all(8), 269 | physics: const NeverScrollableScrollPhysics(), 270 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 271 | crossAxisCount: 3, 272 | crossAxisSpacing: 8, 273 | mainAxisSpacing: 8, 274 | childAspectRatio: cellSizeRatio), 275 | children: [ 276 | FilledButton( 277 | onPressed: () { 278 | if (inputBFN.hasFocus) { 279 | inputBFN.unfocus(); 280 | inputAFN.requestFocus(); 281 | } else if (inputAFN.hasFocus) { 282 | inputAFN.unfocus(); 283 | inputBFN.requestFocus(); 284 | } 285 | }, 286 | child: Transform.rotate( 287 | angle: 90 * pi / 180, 288 | child: const Icon( 289 | Icons.compare_arrows, 290 | size: 32, 291 | ), 292 | )), 293 | _buildButtons("C", false), 294 | FilledButton( 295 | onPressed: () { 296 | _bkspc(); 297 | 298 | HapticFeedback.lightImpact(); 299 | }, 300 | child: const Icon( 301 | Icons.backspace_outlined, 302 | size: 32, 303 | )), 304 | _buildButtons("7", true), 305 | _buildButtons("8", true), 306 | _buildButtons("9", true), 307 | _buildButtons("4", true), 308 | _buildButtons("5", true), 309 | _buildButtons("6", true), 310 | _buildButtons("1", true), 311 | _buildButtons("2", true), 312 | _buildButtons("3", true), 313 | const FilledButton.tonal( 314 | onPressed: null, 315 | child: Text( 316 | "\u00b1", 317 | style: TextStyle( 318 | fontSize: 32, 319 | ), 320 | )), 321 | _buildButtons("0", true), 322 | _buildButtons(".", true), 323 | ], 324 | ); 325 | } 326 | 327 | Widget _inputView(BuildContext context, double fontsize) { 328 | return Container( 329 | decoration: BoxDecoration( 330 | borderRadius: BorderRadius.circular(16), 331 | color: Theme.of(context).colorScheme.secondaryContainer, 332 | ), 333 | padding: const EdgeInsets.all(8), 334 | margin: const EdgeInsets.all(8), 335 | child: Column( 336 | crossAxisAlignment: CrossAxisAlignment.start, 337 | mainAxisAlignment: MainAxisAlignment.start, 338 | children: [ 339 | DropdownMenu( 340 | dropdownMenuEntries: List.generate( 341 | units.length, 342 | growable: false, 343 | (index) { 344 | var unit = units[index]; 345 | return DropdownMenuEntry( 346 | value: unit.name, 347 | label: 348 | unit.name.toString().split("ENERGY.").last.capitalize(), 349 | ); 350 | }, 351 | ), 352 | initialSelection: selectedenergyA, 353 | onSelected: (value) { 354 | setState(() { 355 | selectedenergyA = value; 356 | }); 357 | if (inputAFN.hasFocus) { 358 | if (inputA.text.isNotEmpty) { 359 | energy.convert(value, double.parse(inputA.text)); 360 | units = energy.getAll(); 361 | 362 | _convValueBuild(units); 363 | inputB.text = unitDetails[selectedenergyB] ?? ""; 364 | } 365 | } else if (inputBFN.hasFocus) { 366 | if (inputB.text.isNotEmpty) { 367 | energy.convert(selectedenergyB, double.parse(inputB.text)); 368 | units = energy.getAll(); 369 | 370 | _convValueBuild(units); 371 | inputA.text = unitDetails[value] ?? ""; 372 | } 373 | } 374 | }, 375 | ), 376 | TextField( 377 | enableSuggestions: false, 378 | textAlign: TextAlign.right, 379 | decoration: InputDecoration( 380 | border: InputBorder.none, 381 | suffixText: selectedenergySymbolA.toString(), 382 | ), 383 | controller: inputA, 384 | focusNode: inputAFN, 385 | onChanged: (value) { 386 | if (inputAFN.hasFocus) { 387 | _conv(selectedenergyA, value, inputB); 388 | } 389 | }, 390 | inputFormatters: [ 391 | FilteringTextInputFormatter.deny(RegExp(r'[a-z] [A-Z] :$')) 392 | ], 393 | style: TextStyle( 394 | fontSize: fontsize, 395 | ), 396 | keyboardType: TextInputType.none, 397 | ), 398 | const Divider(), 399 | DropdownMenu( 400 | dropdownMenuEntries: List.generate( 401 | units.length, 402 | growable: false, 403 | (index) { 404 | var unit = units[index]; 405 | return DropdownMenuEntry( 406 | value: unit.name, 407 | label: 408 | unit.name.toString().split("ENERGY.").last.capitalize(), 409 | ); 410 | }, 411 | ), 412 | initialSelection: selectedenergyB, 413 | onSelected: (value) { 414 | setState(() { 415 | selectedenergyB = value; 416 | }); 417 | if (inputBFN.hasFocus) { 418 | if (inputB.text.isNotEmpty) { 419 | energy.convert(value, double.parse(inputB.text)); 420 | units = energy.getAll(); 421 | 422 | _convValueBuild(units); 423 | inputA.text = unitDetails[selectedenergyA] ?? ""; 424 | } 425 | } else if (inputAFN.hasFocus) { 426 | if (inputA.text.isNotEmpty) { 427 | energy.convert(selectedenergyA, double.parse(inputA.text)); 428 | units = energy.getAll(); 429 | 430 | _convValueBuild(units); 431 | inputB.text = unitDetails[value] ?? ""; 432 | } 433 | } 434 | }, 435 | ), 436 | TextField( 437 | enableSuggestions: false, 438 | textAlign: TextAlign.right, 439 | decoration: InputDecoration( 440 | border: InputBorder.none, 441 | suffixText: selectedenergySymbolB.toString(), 442 | ), 443 | controller: inputB, 444 | focusNode: inputBFN, 445 | onChanged: (value) { 446 | if (inputBFN.hasFocus) { 447 | _conv(selectedenergyB, value, inputA); 448 | } 449 | }, 450 | inputFormatters: [ 451 | FilteringTextInputFormatter.deny(RegExp(r'[a-z] [A-Z] :$')) 452 | ], 453 | style: TextStyle( 454 | fontSize: fontsize, 455 | ), 456 | keyboardType: TextInputType.none, 457 | ), 458 | ], 459 | ), 460 | ); 461 | } 462 | 463 | Widget _buildButtons(String label, bool tonal) { 464 | return tonal 465 | ? SizedBox( 466 | height: 32, 467 | width: 72, 468 | child: FilledButton.tonal( 469 | onPressed: () { 470 | _convFunc(label); 471 | HapticFeedback.lightImpact(); 472 | }, 473 | child: Text( 474 | label, 475 | style: const TextStyle( 476 | fontSize: 32, 477 | ), 478 | )), 479 | ) 480 | : SizedBox( 481 | height: 32, 482 | width: 72, 483 | child: FilledButton( 484 | onPressed: () { 485 | _convFunc(label); 486 | HapticFeedback.lightImpact(); 487 | }, 488 | child: Text( 489 | label, 490 | style: const TextStyle( 491 | fontSize: 32, 492 | ), 493 | )), 494 | ); 495 | } 496 | } 497 | -------------------------------------------------------------------------------- /lib/pages/conv/length_conv.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:responsive_builder/responsive_builder.dart'; 7 | import 'package:units_converter/units_converter.dart'; 8 | 9 | import '../../models/settings_model.dart'; 10 | import '../settings_page.dart'; 11 | 12 | class LengthConv extends StatefulWidget { 13 | const LengthConv({Key? key}) : super(key: key); 14 | static String pageTitle = "Length"; 15 | 16 | @override 17 | State createState() => _LengthConvState(); 18 | } 19 | 20 | class _LengthConvState extends State { 21 | TextEditingController inputA = TextEditingController(); 22 | FocusNode inputAFN = FocusNode(); 23 | TextEditingController inputB = TextEditingController(); 24 | FocusNode inputBFN = FocusNode(); 25 | 26 | var selectedlengthA; 27 | var selectedlengthB; 28 | var selectedlengthSymbolA; 29 | var selectedlengthSymbolB; 30 | var length = Length(significantFigures: 7, removeTrailingZeros: true); 31 | 32 | var units = []; 33 | 34 | void _bkspc() { 35 | if (inputAFN.hasFocus) { 36 | if (inputA.text.isNotEmpty) { 37 | if (inputA.selection.isCollapsed) { 38 | if (inputA.selection.baseOffset == inputA.text.length) { 39 | setState(() { 40 | inputA.text = inputA.text.substring(0, inputA.text.length - 1); 41 | }); 42 | inputA.selection = TextSelection.fromPosition( 43 | TextPosition(offset: inputA.text.length)); 44 | } else { 45 | setState(() { 46 | inputA.value = inputA.value.replaced( 47 | TextRange( 48 | start: inputA.selection.baseOffset - 1, 49 | end: inputA.selection.baseOffset), 50 | ""); 51 | }); 52 | inputA.selection = TextSelection.fromPosition( 53 | TextPosition(offset: inputA.selection.start)); 54 | } 55 | } else { 56 | setState(() { 57 | inputA.value = inputA.value.replaced( 58 | TextRange( 59 | start: inputA.selection.start, end: inputA.selection.end), 60 | ""); 61 | }); 62 | inputA.selection = TextSelection.fromPosition( 63 | TextPosition(offset: inputA.selection.end)); 64 | } 65 | if (inputA.text.isNotEmpty) { 66 | length.convert(selectedlengthA, double.parse(inputA.text)); 67 | 68 | units = length.getAll(); 69 | 70 | _convValueBuild(units); 71 | inputB.text = unitDetails[selectedlengthB] ?? ""; 72 | } else { 73 | setState(() { 74 | inputA.clear(); 75 | inputB.clear(); 76 | }); 77 | } 78 | } 79 | } else if (inputBFN.hasFocus) { 80 | if (inputB.text.isNotEmpty) { 81 | if (inputB.selection.isCollapsed) { 82 | if (inputB.selection.baseOffset == inputB.text.length) { 83 | setState(() { 84 | inputB.text = inputB.text.substring(0, inputB.text.length - 1); 85 | }); 86 | inputB.selection = TextSelection.fromPosition( 87 | TextPosition(offset: inputB.text.length)); 88 | } else { 89 | setState(() { 90 | inputB.value = inputB.value.replaced( 91 | TextRange( 92 | start: inputB.selection.baseOffset - 1, 93 | end: inputB.selection.baseOffset), 94 | ""); 95 | }); 96 | inputB.selection = TextSelection.fromPosition( 97 | TextPosition(offset: inputB.selection.start)); 98 | } 99 | } else { 100 | setState(() { 101 | inputB.value = inputB.value.replaced( 102 | TextRange( 103 | start: inputB.selection.start, end: inputB.selection.end), 104 | ""); 105 | }); 106 | inputB.selection = TextSelection.fromPosition( 107 | TextPosition(offset: inputB.selection.end)); 108 | } 109 | if (inputB.text.isNotEmpty) { 110 | length.convert(selectedlengthB, double.parse(inputB.text)); 111 | units = length.getAll(); 112 | 113 | _convValueBuild(units); 114 | inputA.text = unitDetails[selectedlengthA] ?? ""; 115 | } else { 116 | setState(() { 117 | inputA.clear(); 118 | inputB.clear(); 119 | }); 120 | } 121 | } 122 | } 123 | } 124 | 125 | Map unitDetails = {}; 126 | 127 | void _convValueBuild(unitsconv) { 128 | for (Unit unit in unitsconv) { 129 | if (unit.name == selectedlengthA) { 130 | setState(() { 131 | selectedlengthSymbolA = unit.symbol; 132 | }); 133 | } else if (unit.name == selectedlengthB) { 134 | setState(() { 135 | selectedlengthSymbolB = unit.symbol; 136 | }); 137 | } 138 | unitDetails.addAll({unit.name: unit.stringValue ?? ""}); 139 | } 140 | } 141 | 142 | void _conv(selectedUnit, val, TextEditingController input) { 143 | length.convert(selectedUnit, val); 144 | 145 | units = length.getAll(); 146 | 147 | _convValueBuild(units); 148 | input.text = unitDetails[selectedUnit] ?? ""; 149 | } 150 | 151 | void _convFunc(val) { 152 | if (val == "C") { 153 | inputA.clear(); 154 | inputB.clear(); 155 | } else { 156 | setState(() { 157 | if (inputAFN.hasFocus) { 158 | inputA.value = TextEditingValue( 159 | text: inputA.text.replaceRange( 160 | inputA.selection.start, inputA.selection.end, val), 161 | selection: TextSelection.collapsed( 162 | offset: inputA.selection.baseOffset + val.toString().length), 163 | ); 164 | length.convert(selectedlengthA, double.parse(inputA.text)); 165 | 166 | units = length.getAll(); 167 | 168 | _convValueBuild(units); 169 | inputB.text = unitDetails[selectedlengthB] ?? ""; 170 | } else if (inputBFN.hasFocus) { 171 | inputB.value = TextEditingValue( 172 | text: inputB.text.replaceRange( 173 | inputB.selection.start, inputB.selection.end, val), 174 | selection: TextSelection.collapsed( 175 | offset: inputB.selection.baseOffset + val.toString().length), 176 | ); 177 | length.convert(selectedlengthB, double.parse(inputB.text)); 178 | 179 | units = length.getAll(); 180 | 181 | _convValueBuild(units); 182 | inputA.text = unitDetails[selectedlengthA] ?? ""; 183 | } 184 | }); 185 | } 186 | } 187 | 188 | @override 189 | void initState() { 190 | super.initState(); 191 | setState(() { 192 | units = length.getAll(); 193 | selectedlengthA = LENGTH.meters; 194 | selectedlengthB = LENGTH.centimeters; 195 | }); 196 | inputAFN.requestFocus(); 197 | _convValueBuild(units); 198 | } 199 | 200 | @override 201 | Widget build(BuildContext context) { 202 | SettingsModel settings = Provider.of(context); 203 | length.significantFigures = settings.sigFig; 204 | return Scaffold( 205 | resizeToAvoidBottomInset: false, 206 | body: SafeArea( 207 | child: ResponsiveBuilder( 208 | builder: (context, sizingInformation) { 209 | if (sizingInformation.deviceScreenType == DeviceScreenType.tablet) { 210 | return OrientationBuilder( 211 | builder: (context, orientation) { 212 | if (orientation == Orientation.landscape) { 213 | return Row( 214 | crossAxisAlignment: CrossAxisAlignment.center, 215 | mainAxisAlignment: MainAxisAlignment.center, 216 | children: [ 217 | Expanded( 218 | child: _inputView(context, 48), 219 | ), 220 | Expanded(child: _keypad(context, 1.42)) 221 | ], 222 | ); 223 | } else { 224 | return Column( 225 | crossAxisAlignment: CrossAxisAlignment.center, 226 | mainAxisAlignment: MainAxisAlignment.center, 227 | children: [ 228 | Expanded( 229 | child: _inputView(context, 48), 230 | ), 231 | _keypad(context, 2) 232 | ], 233 | ); 234 | } 235 | }, 236 | ); 237 | } 238 | return OrientationBuilder( 239 | builder: (context, orientation) { 240 | if (orientation == Orientation.landscape) { 241 | return Row( 242 | children: [ 243 | Expanded(child: _inputView(context, 32)), 244 | Expanded(child: _keypad(context, 2.4)), 245 | ], 246 | ); 247 | } else { 248 | return Column( 249 | children: [ 250 | Expanded( 251 | flex: 1, 252 | child: _inputView(context, 48), 253 | ), 254 | _keypad(context, 1.8) 255 | ], 256 | ); 257 | } 258 | }, 259 | ); 260 | }, 261 | ), 262 | ), 263 | ); 264 | } 265 | 266 | Widget _keypad(BuildContext context, double cellSizeRatio) { 267 | return GridView( 268 | shrinkWrap: true, 269 | padding: const EdgeInsets.all(8), 270 | physics: const NeverScrollableScrollPhysics(), 271 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 272 | crossAxisCount: 3, 273 | crossAxisSpacing: 8, 274 | mainAxisSpacing: 8, 275 | childAspectRatio: cellSizeRatio), 276 | children: [ 277 | FilledButton( 278 | onPressed: () { 279 | if (inputBFN.hasFocus) { 280 | inputBFN.unfocus(); 281 | inputAFN.requestFocus(); 282 | } else if (inputAFN.hasFocus) { 283 | inputAFN.unfocus(); 284 | inputBFN.requestFocus(); 285 | } 286 | }, 287 | child: Transform.rotate( 288 | angle: 90 * pi / 180, 289 | child: const Icon( 290 | Icons.compare_arrows, 291 | size: 32, 292 | ), 293 | )), 294 | _buildButtons("C", false), 295 | FilledButton( 296 | onPressed: () { 297 | _bkspc(); 298 | 299 | HapticFeedback.lightImpact(); 300 | }, 301 | child: const Icon( 302 | Icons.backspace_outlined, 303 | size: 32, 304 | )), 305 | _buildButtons("7", true), 306 | _buildButtons("8", true), 307 | _buildButtons("9", true), 308 | _buildButtons("4", true), 309 | _buildButtons("5", true), 310 | _buildButtons("6", true), 311 | _buildButtons("1", true), 312 | _buildButtons("2", true), 313 | _buildButtons("3", true), 314 | const FilledButton.tonal( 315 | onPressed: null, 316 | child: Text( 317 | "\u00b1", 318 | style: TextStyle( 319 | fontSize: 32, 320 | ), 321 | )), 322 | _buildButtons("0", true), 323 | _buildButtons(".", true), 324 | ], 325 | ); 326 | } 327 | 328 | Widget _inputView(BuildContext context, double fontsize) { 329 | return Container( 330 | decoration: BoxDecoration( 331 | borderRadius: BorderRadius.circular(16), 332 | color: Theme.of(context).colorScheme.secondaryContainer, 333 | ), 334 | padding: const EdgeInsets.all(8), 335 | margin: const EdgeInsets.all(8), 336 | child: Column( 337 | crossAxisAlignment: CrossAxisAlignment.start, 338 | mainAxisAlignment: MainAxisAlignment.start, 339 | children: [ 340 | DropdownMenu( 341 | dropdownMenuEntries: List.generate( 342 | units.length, 343 | growable: false, 344 | (index) { 345 | var unit = units[index]; 346 | return DropdownMenuEntry( 347 | value: unit.name, 348 | label: 349 | unit.name.toString().split("LENGTH.").last.capitalize(), 350 | ); 351 | }, 352 | ), 353 | initialSelection: selectedlengthA, 354 | onSelected: (value) { 355 | setState(() { 356 | selectedlengthA = value; 357 | }); 358 | if (inputAFN.hasFocus) { 359 | if (inputA.text.isNotEmpty) { 360 | length.convert(value, double.parse(inputA.text)); 361 | units = length.getAll(); 362 | 363 | _convValueBuild(units); 364 | inputB.text = unitDetails[selectedlengthB] ?? ""; 365 | } 366 | } else if (inputBFN.hasFocus) { 367 | if (inputB.text.isNotEmpty) { 368 | length.convert(selectedlengthB, double.parse(inputB.text)); 369 | units = length.getAll(); 370 | 371 | _convValueBuild(units); 372 | inputA.text = unitDetails[value] ?? ""; 373 | } 374 | } 375 | }, 376 | ), 377 | TextField( 378 | enableSuggestions: false, 379 | textAlign: TextAlign.right, 380 | decoration: InputDecoration( 381 | border: InputBorder.none, 382 | suffixText: selectedlengthSymbolA.toString(), 383 | ), 384 | controller: inputA, 385 | focusNode: inputAFN, 386 | onChanged: (value) { 387 | if (inputAFN.hasFocus) { 388 | _conv(selectedlengthA, value, inputB); 389 | } 390 | }, 391 | inputFormatters: [ 392 | FilteringTextInputFormatter.deny(RegExp(r'[a-z] [A-Z] :$')) 393 | ], 394 | style: TextStyle( 395 | fontSize: fontsize, 396 | ), 397 | keyboardType: TextInputType.none, 398 | ), 399 | const Divider(), 400 | DropdownMenu( 401 | dropdownMenuEntries: List.generate( 402 | units.length, 403 | growable: false, 404 | (index) { 405 | var unit = units[index]; 406 | return DropdownMenuEntry( 407 | value: unit.name, 408 | label: 409 | unit.name.toString().split("LENGTH.").last.capitalize(), 410 | ); 411 | }, 412 | ), 413 | initialSelection: selectedlengthB, 414 | onSelected: (value) { 415 | setState(() { 416 | selectedlengthB = value; 417 | }); 418 | if (inputBFN.hasFocus) { 419 | if (inputB.text.isNotEmpty) { 420 | length.convert(value, double.parse(inputB.text)); 421 | units = length.getAll(); 422 | 423 | _convValueBuild(units); 424 | inputA.text = unitDetails[selectedlengthA] ?? ""; 425 | } 426 | } else if (inputAFN.hasFocus) { 427 | if (inputA.text.isNotEmpty) { 428 | length.convert(selectedlengthA, double.parse(inputA.text)); 429 | units = length.getAll(); 430 | 431 | _convValueBuild(units); 432 | inputB.text = unitDetails[value] ?? ""; 433 | } 434 | } 435 | }, 436 | ), 437 | TextField( 438 | enableSuggestions: false, 439 | textAlign: TextAlign.right, 440 | decoration: InputDecoration( 441 | border: InputBorder.none, 442 | suffixText: selectedlengthSymbolB.toString(), 443 | ), 444 | controller: inputB, 445 | focusNode: inputBFN, 446 | onChanged: (value) { 447 | if (inputBFN.hasFocus) { 448 | _conv(selectedlengthB, value, inputA); 449 | } 450 | }, 451 | inputFormatters: [ 452 | FilteringTextInputFormatter.deny(RegExp(r'[a-z] [A-Z] :$')) 453 | ], 454 | style: TextStyle( 455 | fontSize: fontsize, 456 | ), 457 | keyboardType: TextInputType.none, 458 | ), 459 | ], 460 | ), 461 | ); 462 | } 463 | 464 | Widget _buildButtons(String label, bool tonal) { 465 | return tonal 466 | ? SizedBox( 467 | height: 32, 468 | width: 72, 469 | child: FilledButton.tonal( 470 | onPressed: () { 471 | _convFunc(label); 472 | HapticFeedback.lightImpact(); 473 | }, 474 | child: Text( 475 | label, 476 | style: const TextStyle( 477 | fontSize: 32, 478 | ), 479 | )), 480 | ) 481 | : SizedBox( 482 | height: 32, 483 | width: 72, 484 | child: FilledButton( 485 | onPressed: () { 486 | _convFunc(label); 487 | HapticFeedback.lightImpact(); 488 | }, 489 | child: Text( 490 | label, 491 | style: const TextStyle( 492 | fontSize: 32, 493 | ), 494 | )), 495 | ); 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /lib/pages/conv/mass_conv.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:responsive_builder/responsive_builder.dart'; 7 | import 'package:units_converter/units_converter.dart'; 8 | 9 | import '../../models/settings_model.dart'; 10 | import '../settings_page.dart'; 11 | 12 | class MassConv extends StatefulWidget { 13 | const MassConv({Key? key}) : super(key: key); 14 | static String pageTitle = "Mass"; 15 | 16 | @override 17 | State createState() => _MassConvState(); 18 | } 19 | 20 | class _MassConvState extends State { 21 | TextEditingController inputA = TextEditingController(); 22 | FocusNode inputAFN = FocusNode(); 23 | TextEditingController inputB = TextEditingController(); 24 | FocusNode inputBFN = FocusNode(); 25 | 26 | var selectedMassA; 27 | var selectedMassB; 28 | var selectedMassSymbolA; 29 | var selectedMassSymbolB; 30 | var mass = Mass(significantFigures: 7, removeTrailingZeros: true); 31 | var units = []; 32 | 33 | void _bkspc() { 34 | if (inputAFN.hasFocus) { 35 | if (inputA.text.isNotEmpty) { 36 | if (inputA.selection.isCollapsed) { 37 | if (inputA.selection.baseOffset == inputA.text.length) { 38 | setState(() { 39 | inputA.text = inputA.text.substring(0, inputA.text.length - 1); 40 | }); 41 | inputA.selection = TextSelection.fromPosition( 42 | TextPosition(offset: inputA.text.length)); 43 | } else { 44 | setState(() { 45 | inputA.value = inputA.value.replaced( 46 | TextRange( 47 | start: inputA.selection.baseOffset - 1, 48 | end: inputA.selection.baseOffset), 49 | ""); 50 | }); 51 | inputA.selection = TextSelection.fromPosition( 52 | TextPosition(offset: inputA.selection.start)); 53 | } 54 | } else { 55 | setState(() { 56 | inputA.value = inputA.value.replaced( 57 | TextRange( 58 | start: inputA.selection.start, end: inputA.selection.end), 59 | ""); 60 | }); 61 | inputA.selection = TextSelection.fromPosition( 62 | TextPosition(offset: inputA.selection.end)); 63 | } 64 | if (inputA.text.isNotEmpty) { 65 | mass.convert(selectedMassA, double.parse(inputA.text)); 66 | 67 | units = mass.getAll(); 68 | 69 | _convValueBuild(units); 70 | inputB.text = unitDetails[selectedMassB] ?? ""; 71 | } else { 72 | setState(() { 73 | inputA.clear(); 74 | inputB.clear(); 75 | }); 76 | } 77 | } 78 | } else if (inputBFN.hasFocus) { 79 | if (inputB.text.isNotEmpty) { 80 | if (inputB.selection.isCollapsed) { 81 | if (inputB.selection.baseOffset == inputB.text.length) { 82 | setState(() { 83 | inputB.text = inputB.text.substring(0, inputB.text.length - 1); 84 | }); 85 | inputB.selection = TextSelection.fromPosition( 86 | TextPosition(offset: inputB.text.length)); 87 | } else { 88 | setState(() { 89 | inputB.value = inputB.value.replaced( 90 | TextRange( 91 | start: inputB.selection.baseOffset - 1, 92 | end: inputB.selection.baseOffset), 93 | ""); 94 | }); 95 | inputB.selection = TextSelection.fromPosition( 96 | TextPosition(offset: inputB.selection.start)); 97 | } 98 | } else { 99 | setState(() { 100 | inputB.value = inputB.value.replaced( 101 | TextRange( 102 | start: inputB.selection.start, end: inputB.selection.end), 103 | ""); 104 | }); 105 | inputB.selection = TextSelection.fromPosition( 106 | TextPosition(offset: inputB.selection.end)); 107 | } 108 | if (inputB.text.isNotEmpty) { 109 | mass.convert(selectedMassB, double.parse(inputB.text)); 110 | units = mass.getAll(); 111 | 112 | _convValueBuild(units); 113 | inputA.text = unitDetails[selectedMassA] ?? ""; 114 | } else { 115 | setState(() { 116 | inputA.clear(); 117 | inputB.clear(); 118 | }); 119 | } 120 | } 121 | } 122 | } 123 | 124 | Map unitDetails = {}; 125 | 126 | void _convValueBuild(unitsconv) { 127 | for (Unit unit in unitsconv) { 128 | if (unit.name == selectedMassA) { 129 | setState(() { 130 | selectedMassSymbolA = unit.symbol; 131 | }); 132 | } else if (unit.name == selectedMassB) { 133 | setState(() { 134 | selectedMassSymbolB = unit.symbol; 135 | }); 136 | } 137 | unitDetails.addAll({unit.name: unit.stringValue ?? ""}); 138 | } 139 | } 140 | 141 | void _conv(selectedUnit, val, TextEditingController input) { 142 | mass.convert(selectedUnit, val); 143 | 144 | units = mass.getAll(); 145 | 146 | _convValueBuild(units); 147 | input.text = unitDetails[selectedUnit] ?? ""; 148 | } 149 | 150 | void _convFunc(val) { 151 | if (val == "C") { 152 | inputA.clear(); 153 | inputB.clear(); 154 | } else { 155 | setState(() { 156 | if (inputAFN.hasFocus) { 157 | inputA.value = TextEditingValue( 158 | text: inputA.text.replaceRange( 159 | inputA.selection.start, inputA.selection.end, val), 160 | selection: TextSelection.collapsed( 161 | offset: inputA.selection.baseOffset + val.toString().length), 162 | ); 163 | mass.convert(selectedMassA, double.parse(inputA.text)); 164 | 165 | units = mass.getAll(); 166 | 167 | _convValueBuild(units); 168 | inputB.text = unitDetails[selectedMassB] ?? ""; 169 | } else if (inputBFN.hasFocus) { 170 | inputB.value = TextEditingValue( 171 | text: inputB.text.replaceRange( 172 | inputB.selection.start, inputB.selection.end, val), 173 | selection: TextSelection.collapsed( 174 | offset: inputB.selection.baseOffset + val.toString().length), 175 | ); 176 | mass.convert(selectedMassB, double.parse(inputB.text)); 177 | 178 | units = mass.getAll(); 179 | 180 | _convValueBuild(units); 181 | inputA.text = unitDetails[selectedMassA] ?? ""; 182 | } 183 | }); 184 | } 185 | } 186 | 187 | @override 188 | void initState() { 189 | super.initState(); 190 | setState(() { 191 | units = mass.getAll(); 192 | selectedMassA = MASS.kilograms; 193 | selectedMassB = MASS.grams; 194 | }); 195 | inputAFN.requestFocus(); 196 | _convValueBuild(units); 197 | } 198 | 199 | @override 200 | Widget build(BuildContext context) { 201 | SettingsModel settings = Provider.of(context); 202 | mass.significantFigures = settings.sigFig; 203 | return Scaffold( 204 | resizeToAvoidBottomInset: false, 205 | body: SafeArea( 206 | child: ResponsiveBuilder( 207 | builder: (context, sizingInformation) { 208 | if (sizingInformation.deviceScreenType == DeviceScreenType.tablet) { 209 | return OrientationBuilder( 210 | builder: (context, orientation) { 211 | if (orientation == Orientation.landscape) { 212 | return Row( 213 | crossAxisAlignment: CrossAxisAlignment.center, 214 | mainAxisAlignment: MainAxisAlignment.center, 215 | children: [ 216 | Expanded( 217 | child: _inputView(context, 48), 218 | ), 219 | Expanded(child: _keypad(context, 1.42)) 220 | ], 221 | ); 222 | } else { 223 | return Column( 224 | crossAxisAlignment: CrossAxisAlignment.center, 225 | mainAxisAlignment: MainAxisAlignment.center, 226 | children: [ 227 | Expanded( 228 | child: _inputView(context, 48), 229 | ), 230 | _keypad(context, 2) 231 | ], 232 | ); 233 | } 234 | }, 235 | ); 236 | } 237 | return OrientationBuilder( 238 | builder: (context, orientation) { 239 | if (orientation == Orientation.landscape) { 240 | return Row( 241 | children: [ 242 | Expanded(child: _inputView(context, 32)), 243 | Expanded(child: _keypad(context, 2.4)), 244 | ], 245 | ); 246 | } else { 247 | return Column( 248 | children: [ 249 | Expanded( 250 | flex: 1, 251 | child: _inputView(context, 48), 252 | ), 253 | _keypad(context, 1.8) 254 | ], 255 | ); 256 | } 257 | }, 258 | ); 259 | }, 260 | ), 261 | ), 262 | ); 263 | } 264 | 265 | Widget _keypad(BuildContext context, double cellSizeRatio) { 266 | return GridView( 267 | shrinkWrap: true, 268 | padding: const EdgeInsets.all(8), 269 | physics: const NeverScrollableScrollPhysics(), 270 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 271 | crossAxisCount: 3, 272 | crossAxisSpacing: 8, 273 | mainAxisSpacing: 8, 274 | childAspectRatio: cellSizeRatio), 275 | children: [ 276 | FilledButton( 277 | onPressed: () { 278 | if (inputBFN.hasFocus) { 279 | inputBFN.unfocus(); 280 | inputAFN.requestFocus(); 281 | } else if (inputAFN.hasFocus) { 282 | inputAFN.unfocus(); 283 | inputBFN.requestFocus(); 284 | } 285 | }, 286 | child: Transform.rotate( 287 | angle: 90 * pi / 180, 288 | child: const Icon( 289 | Icons.compare_arrows, 290 | size: 32, 291 | ), 292 | )), 293 | _buildButtons("C", false), 294 | FilledButton( 295 | onPressed: () { 296 | _bkspc(); 297 | 298 | HapticFeedback.lightImpact(); 299 | }, 300 | child: const Icon( 301 | Icons.backspace_outlined, 302 | size: 32, 303 | )), 304 | _buildButtons("7", true), 305 | _buildButtons("8", true), 306 | _buildButtons("9", true), 307 | _buildButtons("4", true), 308 | _buildButtons("5", true), 309 | _buildButtons("6", true), 310 | _buildButtons("1", true), 311 | _buildButtons("2", true), 312 | _buildButtons("3", true), 313 | const FilledButton.tonal( 314 | onPressed: null, 315 | child: Text( 316 | "\u00b1", 317 | style: TextStyle( 318 | fontSize: 32, 319 | ), 320 | )), 321 | _buildButtons("0", true), 322 | _buildButtons(".", true), 323 | ], 324 | ); 325 | } 326 | 327 | Widget _inputView(BuildContext context, double fontsize) { 328 | return Container( 329 | decoration: BoxDecoration( 330 | borderRadius: BorderRadius.circular(16), 331 | color: Theme.of(context).colorScheme.secondaryContainer, 332 | ), 333 | padding: const EdgeInsets.all(8), 334 | margin: const EdgeInsets.all(8), 335 | child: Column( 336 | crossAxisAlignment: CrossAxisAlignment.start, 337 | mainAxisAlignment: MainAxisAlignment.start, 338 | children: [ 339 | DropdownMenu( 340 | dropdownMenuEntries: List.generate( 341 | units.length, 342 | growable: false, 343 | (index) { 344 | var unit = units[index]; 345 | return DropdownMenuEntry( 346 | value: unit.name, 347 | label: unit.name.toString().split("MASS.").last.capitalize(), 348 | ); 349 | }, 350 | ), 351 | initialSelection: selectedMassA, 352 | onSelected: (value) { 353 | setState(() { 354 | selectedMassA = value; 355 | }); 356 | if (inputAFN.hasFocus) { 357 | if (inputA.text.isNotEmpty) { 358 | mass.convert(value, double.parse(inputA.text)); 359 | units = mass.getAll(); 360 | 361 | _convValueBuild(units); 362 | inputB.text = unitDetails[selectedMassB] ?? ""; 363 | } 364 | } else if (inputBFN.hasFocus) { 365 | if (inputB.text.isNotEmpty) { 366 | mass.convert(selectedMassB, double.parse(inputB.text)); 367 | units = mass.getAll(); 368 | 369 | _convValueBuild(units); 370 | inputA.text = unitDetails[value] ?? ""; 371 | } 372 | } 373 | }, 374 | ), 375 | TextField( 376 | enableSuggestions: false, 377 | textAlign: TextAlign.right, 378 | decoration: InputDecoration( 379 | border: InputBorder.none, 380 | suffixText: selectedMassSymbolA.toString(), 381 | ), 382 | controller: inputA, 383 | focusNode: inputAFN, 384 | onChanged: (value) { 385 | if (inputAFN.hasFocus) { 386 | _conv(selectedMassA, value, inputB); 387 | } 388 | }, 389 | inputFormatters: [ 390 | FilteringTextInputFormatter.deny(RegExp(r'[a-z] [A-Z] :$')) 391 | ], 392 | style: TextStyle( 393 | fontSize: fontsize, 394 | ), 395 | keyboardType: TextInputType.none, 396 | ), 397 | const Divider(), 398 | DropdownMenu( 399 | dropdownMenuEntries: List.generate( 400 | units.length, 401 | growable: false, 402 | (index) { 403 | var unit = units[index]; 404 | return DropdownMenuEntry( 405 | value: unit.name, 406 | label: unit.name.toString().split("MASS.").last.capitalize(), 407 | ); 408 | }, 409 | ), 410 | initialSelection: selectedMassB, 411 | onSelected: (value) { 412 | setState(() { 413 | selectedMassB = value; 414 | }); 415 | if (inputBFN.hasFocus) { 416 | if (inputB.text.isNotEmpty) { 417 | mass.convert(value, double.parse(inputB.text)); 418 | units = mass.getAll(); 419 | 420 | _convValueBuild(units); 421 | inputA.text = unitDetails[selectedMassA] ?? ""; 422 | } 423 | } else if (inputAFN.hasFocus) { 424 | if (inputA.text.isNotEmpty) { 425 | mass.convert(selectedMassA, double.parse(inputA.text)); 426 | units = mass.getAll(); 427 | 428 | _convValueBuild(units); 429 | inputB.text = unitDetails[value] ?? ""; 430 | } 431 | } 432 | }, 433 | ), 434 | TextField( 435 | enableSuggestions: false, 436 | textAlign: TextAlign.right, 437 | decoration: InputDecoration( 438 | border: InputBorder.none, 439 | suffixText: selectedMassSymbolB.toString(), 440 | ), 441 | controller: inputB, 442 | focusNode: inputBFN, 443 | onChanged: (value) { 444 | if (inputBFN.hasFocus) { 445 | _conv(selectedMassB, value, inputA); 446 | } 447 | }, 448 | inputFormatters: [ 449 | FilteringTextInputFormatter.deny(RegExp(r'[a-z] [A-Z] :$')) 450 | ], 451 | style: TextStyle( 452 | fontSize: fontsize, 453 | ), 454 | keyboardType: TextInputType.none, 455 | ), 456 | ], 457 | ), 458 | ); 459 | } 460 | 461 | Widget _buildButtons(String label, bool tonal) { 462 | return tonal 463 | ? SizedBox( 464 | height: 32, 465 | width: 72, 466 | child: FilledButton.tonal( 467 | onPressed: () { 468 | _convFunc(label); 469 | HapticFeedback.lightImpact(); 470 | }, 471 | child: Text( 472 | label, 473 | style: const TextStyle( 474 | fontSize: 32, 475 | ), 476 | )), 477 | ) 478 | : SizedBox( 479 | height: 32, 480 | width: 72, 481 | child: FilledButton( 482 | onPressed: () { 483 | _convFunc(label); 484 | HapticFeedback.lightImpact(); 485 | }, 486 | child: Text( 487 | label, 488 | style: const TextStyle( 489 | fontSize: 32, 490 | ), 491 | )), 492 | ); 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /lib/pages/conv/power_conv.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:responsive_builder/responsive_builder.dart'; 7 | import 'package:units_converter/units_converter.dart'; 8 | 9 | import '../../models/settings_model.dart'; 10 | import '../settings_page.dart'; 11 | 12 | class PowerConv extends StatefulWidget { 13 | const PowerConv({Key? key}) : super(key: key); 14 | static String pageTitle = "Power"; 15 | 16 | @override 17 | State createState() => _PowerConvState(); 18 | } 19 | 20 | class _PowerConvState extends State { 21 | TextEditingController inputA = TextEditingController(); 22 | FocusNode inputAFN = FocusNode(); 23 | TextEditingController inputB = TextEditingController(); 24 | FocusNode inputBFN = FocusNode(); 25 | 26 | var selectedpowerA; 27 | var selectedpowerB; 28 | var selectedpowerSymbolA; 29 | var selectedpowerSymbolB; 30 | var power = Power(significantFigures: 7, removeTrailingZeros: true); 31 | var units = []; 32 | 33 | void _bkspc() { 34 | if (inputAFN.hasFocus) { 35 | if (inputA.text.isNotEmpty) { 36 | if (inputA.selection.isCollapsed) { 37 | if (inputA.selection.baseOffset == inputA.text.length) { 38 | setState(() { 39 | inputA.text = inputA.text.substring(0, inputA.text.length - 1); 40 | }); 41 | inputA.selection = TextSelection.fromPosition( 42 | TextPosition(offset: inputA.text.length)); 43 | } else { 44 | setState(() { 45 | inputA.value = inputA.value.replaced( 46 | TextRange( 47 | start: inputA.selection.baseOffset - 1, 48 | end: inputA.selection.baseOffset), 49 | ""); 50 | }); 51 | inputA.selection = TextSelection.fromPosition( 52 | TextPosition(offset: inputA.selection.start)); 53 | } 54 | } else { 55 | setState(() { 56 | inputA.value = inputA.value.replaced( 57 | TextRange( 58 | start: inputA.selection.start, end: inputA.selection.end), 59 | ""); 60 | }); 61 | inputA.selection = TextSelection.fromPosition( 62 | TextPosition(offset: inputA.selection.end)); 63 | } 64 | if (inputA.text.isNotEmpty) { 65 | power.convert(selectedpowerA, double.parse(inputA.text)); 66 | 67 | units = power.getAll(); 68 | 69 | _convValueBuild(units); 70 | inputB.text = unitDetails[selectedpowerB] ?? ""; 71 | } else { 72 | setState(() { 73 | inputA.clear(); 74 | inputB.clear(); 75 | }); 76 | } 77 | } 78 | } else if (inputBFN.hasFocus) { 79 | if (inputB.text.isNotEmpty) { 80 | if (inputB.selection.isCollapsed) { 81 | if (inputB.selection.baseOffset == inputB.text.length) { 82 | setState(() { 83 | inputB.text = inputB.text.substring(0, inputB.text.length - 1); 84 | }); 85 | inputB.selection = TextSelection.fromPosition( 86 | TextPosition(offset: inputB.text.length)); 87 | } else { 88 | setState(() { 89 | inputB.value = inputB.value.replaced( 90 | TextRange( 91 | start: inputB.selection.baseOffset - 1, 92 | end: inputB.selection.baseOffset), 93 | ""); 94 | }); 95 | inputB.selection = TextSelection.fromPosition( 96 | TextPosition(offset: inputB.selection.start)); 97 | } 98 | } else { 99 | setState(() { 100 | inputB.value = inputB.value.replaced( 101 | TextRange( 102 | start: inputB.selection.start, end: inputB.selection.end), 103 | ""); 104 | }); 105 | inputB.selection = TextSelection.fromPosition( 106 | TextPosition(offset: inputB.selection.end)); 107 | } 108 | if (inputB.text.isNotEmpty) { 109 | power.convert(selectedpowerB, double.parse(inputB.text)); 110 | units = power.getAll(); 111 | 112 | _convValueBuild(units); 113 | inputA.text = unitDetails[selectedpowerA] ?? ""; 114 | } else { 115 | setState(() { 116 | inputA.clear(); 117 | inputB.clear(); 118 | }); 119 | } 120 | } 121 | } 122 | } 123 | 124 | Map unitDetails = {}; 125 | 126 | void _convValueBuild(unitsconv) { 127 | for (Unit unit in unitsconv) { 128 | if (unit.name == selectedpowerA) { 129 | setState(() { 130 | selectedpowerSymbolA = unit.symbol; 131 | }); 132 | } else if (unit.name == selectedpowerB) { 133 | setState(() { 134 | selectedpowerSymbolB = unit.symbol; 135 | }); 136 | } 137 | unitDetails.addAll({unit.name: unit.stringValue ?? ""}); 138 | } 139 | } 140 | 141 | void _conv(selectedUnit, val, TextEditingController input) { 142 | power.convert(selectedUnit, val); 143 | 144 | units = power.getAll(); 145 | 146 | _convValueBuild(units); 147 | input.text = unitDetails[selectedUnit] ?? ""; 148 | } 149 | 150 | void _convFunc(val) { 151 | if (val == "C") { 152 | inputA.clear(); 153 | inputB.clear(); 154 | } else { 155 | setState(() { 156 | if (inputAFN.hasFocus) { 157 | inputA.value = TextEditingValue( 158 | text: inputA.text.replaceRange( 159 | inputA.selection.start, inputA.selection.end, val), 160 | selection: TextSelection.collapsed( 161 | offset: inputA.selection.baseOffset + val.toString().length), 162 | ); 163 | power.convert(selectedpowerA, double.parse(inputA.text)); 164 | 165 | units = power.getAll(); 166 | 167 | _convValueBuild(units); 168 | inputB.text = unitDetails[selectedpowerB] ?? ""; 169 | } else if (inputBFN.hasFocus) { 170 | inputB.value = TextEditingValue( 171 | text: inputB.text.replaceRange( 172 | inputB.selection.start, inputB.selection.end, val), 173 | selection: TextSelection.collapsed( 174 | offset: inputB.selection.baseOffset + val.toString().length), 175 | ); 176 | power.convert(selectedpowerB, double.parse(inputB.text)); 177 | 178 | units = power.getAll(); 179 | 180 | _convValueBuild(units); 181 | inputA.text = unitDetails[selectedpowerA] ?? ""; 182 | } 183 | }); 184 | } 185 | } 186 | 187 | @override 188 | void initState() { 189 | super.initState(); 190 | setState(() { 191 | units = power.getAll(); 192 | selectedpowerA = POWER.watt; 193 | selectedpowerB = POWER.kilowatt; 194 | }); 195 | inputAFN.requestFocus(); 196 | _convValueBuild(units); 197 | } 198 | 199 | @override 200 | Widget build(BuildContext context) { 201 | SettingsModel settings = Provider.of(context); 202 | power.significantFigures = settings.sigFig; 203 | return Scaffold( 204 | resizeToAvoidBottomInset: false, 205 | body: SafeArea( 206 | child: ResponsiveBuilder( 207 | builder: (context, sizingInformation) { 208 | if (sizingInformation.deviceScreenType == DeviceScreenType.tablet) { 209 | return OrientationBuilder( 210 | builder: (context, orientation) { 211 | if (orientation == Orientation.landscape) { 212 | return Row( 213 | crossAxisAlignment: CrossAxisAlignment.center, 214 | mainAxisAlignment: MainAxisAlignment.center, 215 | children: [ 216 | Expanded( 217 | child: _inputView(context, 48), 218 | ), 219 | Expanded(child: _keypad(context, 1.42)) 220 | ], 221 | ); 222 | } else { 223 | return Column( 224 | crossAxisAlignment: CrossAxisAlignment.center, 225 | mainAxisAlignment: MainAxisAlignment.center, 226 | children: [ 227 | Expanded( 228 | child: _inputView(context, 48), 229 | ), 230 | _keypad(context, 2) 231 | ], 232 | ); 233 | } 234 | }, 235 | ); 236 | } 237 | return OrientationBuilder( 238 | builder: (context, orientation) { 239 | if (orientation == Orientation.landscape) { 240 | return Row( 241 | children: [ 242 | Expanded(child: _inputView(context, 32)), 243 | Expanded(child: _keypad(context, 2.4)), 244 | ], 245 | ); 246 | } else { 247 | return Column( 248 | children: [ 249 | Expanded( 250 | flex: 1, 251 | child: _inputView(context, 48), 252 | ), 253 | _keypad(context, 1.8) 254 | ], 255 | ); 256 | } 257 | }, 258 | ); 259 | }, 260 | ), 261 | ), 262 | ); 263 | } 264 | 265 | Widget _keypad(BuildContext context, double cellSizeRatio) { 266 | return GridView( 267 | shrinkWrap: true, 268 | padding: const EdgeInsets.all(8), 269 | physics: const NeverScrollableScrollPhysics(), 270 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 271 | crossAxisCount: 3, 272 | crossAxisSpacing: 8, 273 | mainAxisSpacing: 8, 274 | childAspectRatio: cellSizeRatio), 275 | children: [ 276 | FilledButton( 277 | onPressed: () { 278 | if (inputBFN.hasFocus) { 279 | inputBFN.unfocus(); 280 | inputAFN.requestFocus(); 281 | } else if (inputAFN.hasFocus) { 282 | inputAFN.unfocus(); 283 | inputBFN.requestFocus(); 284 | } 285 | }, 286 | child: Transform.rotate( 287 | angle: 90 * pi / 180, 288 | child: const Icon( 289 | Icons.compare_arrows, 290 | size: 32, 291 | ), 292 | )), 293 | _buildButtons("C", false), 294 | FilledButton( 295 | onPressed: () { 296 | _bkspc(); 297 | 298 | HapticFeedback.lightImpact(); 299 | }, 300 | child: const Icon( 301 | Icons.backspace_outlined, 302 | size: 32, 303 | )), 304 | _buildButtons("7", true), 305 | _buildButtons("8", true), 306 | _buildButtons("9", true), 307 | _buildButtons("4", true), 308 | _buildButtons("5", true), 309 | _buildButtons("6", true), 310 | _buildButtons("1", true), 311 | _buildButtons("2", true), 312 | _buildButtons("3", true), 313 | const FilledButton.tonal( 314 | onPressed: null, 315 | child: Text( 316 | "\u00b1", 317 | style: TextStyle( 318 | fontSize: 32, 319 | ), 320 | )), 321 | _buildButtons("0", true), 322 | _buildButtons(".", true), 323 | ], 324 | ); 325 | } 326 | 327 | Widget _inputView(BuildContext context, double fontsize) { 328 | return Container( 329 | decoration: BoxDecoration( 330 | borderRadius: BorderRadius.circular(16), 331 | color: Theme.of(context).colorScheme.secondaryContainer, 332 | ), 333 | padding: const EdgeInsets.all(8), 334 | margin: const EdgeInsets.all(8), 335 | child: Column( 336 | crossAxisAlignment: CrossAxisAlignment.start, 337 | mainAxisAlignment: MainAxisAlignment.start, 338 | children: [ 339 | DropdownMenu( 340 | dropdownMenuEntries: List.generate( 341 | units.length, 342 | growable: false, 343 | (index) { 344 | var unit = units[index]; 345 | return DropdownMenuEntry( 346 | value: unit.name, 347 | label: unit.name.toString().split("POWER.").last.capitalize(), 348 | ); 349 | }, 350 | ), 351 | initialSelection: selectedpowerA, 352 | onSelected: (value) { 353 | setState(() { 354 | selectedpowerA = value; 355 | }); 356 | if (inputAFN.hasFocus) { 357 | if (inputA.text.isNotEmpty) { 358 | power.convert(value, double.parse(inputA.text)); 359 | units = power.getAll(); 360 | 361 | _convValueBuild(units); 362 | inputB.text = unitDetails[selectedpowerB] ?? ""; 363 | } 364 | } else if (inputBFN.hasFocus) { 365 | if (inputB.text.isNotEmpty) { 366 | power.convert(selectedpowerB, double.parse(inputB.text)); 367 | units = power.getAll(); 368 | 369 | _convValueBuild(units); 370 | inputA.text = unitDetails[value] ?? ""; 371 | } 372 | } 373 | }, 374 | ), 375 | TextField( 376 | enableSuggestions: false, 377 | textAlign: TextAlign.right, 378 | decoration: InputDecoration( 379 | border: InputBorder.none, 380 | suffixText: selectedpowerSymbolA.toString(), 381 | ), 382 | controller: inputA, 383 | focusNode: inputAFN, 384 | onChanged: (value) { 385 | if (inputAFN.hasFocus) { 386 | _conv(selectedpowerA, value, inputB); 387 | } 388 | }, 389 | inputFormatters: [ 390 | FilteringTextInputFormatter.deny(RegExp(r'[a-z] [A-Z] :$')) 391 | ], 392 | style: TextStyle( 393 | fontSize: fontsize, 394 | ), 395 | keyboardType: TextInputType.none, 396 | ), 397 | const Divider(), 398 | DropdownMenu( 399 | dropdownMenuEntries: List.generate( 400 | units.length, 401 | growable: false, 402 | (index) { 403 | var unit = units[index]; 404 | return DropdownMenuEntry( 405 | value: unit.name, 406 | label: unit.name.toString().split("POWER.").last.capitalize(), 407 | ); 408 | }, 409 | ), 410 | initialSelection: selectedpowerB, 411 | onSelected: (value) { 412 | setState(() { 413 | selectedpowerB = value; 414 | }); 415 | if (inputBFN.hasFocus) { 416 | if (inputB.text.isNotEmpty) { 417 | power.convert(value, double.parse(inputB.text)); 418 | units = power.getAll(); 419 | 420 | _convValueBuild(units); 421 | inputA.text = unitDetails[selectedpowerA] ?? ""; 422 | } 423 | } else if (inputAFN.hasFocus) { 424 | if (inputA.text.isNotEmpty) { 425 | power.convert(selectedpowerA, double.parse(inputA.text)); 426 | units = power.getAll(); 427 | 428 | _convValueBuild(units); 429 | inputB.text = unitDetails[value] ?? ""; 430 | } 431 | } 432 | }, 433 | ), 434 | TextField( 435 | enableSuggestions: false, 436 | textAlign: TextAlign.right, 437 | decoration: InputDecoration( 438 | border: InputBorder.none, 439 | suffixText: selectedpowerSymbolB.toString(), 440 | ), 441 | controller: inputB, 442 | focusNode: inputBFN, 443 | onChanged: (value) { 444 | if (inputBFN.hasFocus) { 445 | _conv(selectedpowerB, value, inputA); 446 | } 447 | }, 448 | inputFormatters: [ 449 | FilteringTextInputFormatter.deny(RegExp(r'[a-z] [A-Z] :$')) 450 | ], 451 | style: TextStyle( 452 | fontSize: fontsize, 453 | ), 454 | keyboardType: TextInputType.none, 455 | ), 456 | ], 457 | ), 458 | ); 459 | } 460 | 461 | Widget _buildButtons(String label, bool tonal) { 462 | return tonal 463 | ? SizedBox( 464 | height: 32, 465 | width: 72, 466 | child: FilledButton.tonal( 467 | onPressed: () { 468 | _convFunc(label); 469 | HapticFeedback.lightImpact(); 470 | }, 471 | child: Text( 472 | label, 473 | style: const TextStyle( 474 | fontSize: 32, 475 | ), 476 | )), 477 | ) 478 | : SizedBox( 479 | height: 32, 480 | width: 72, 481 | child: FilledButton( 482 | onPressed: () { 483 | _convFunc(label); 484 | HapticFeedback.lightImpact(); 485 | }, 486 | child: Text( 487 | label, 488 | style: const TextStyle( 489 | fontSize: 32, 490 | ), 491 | )), 492 | ); 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /lib/pages/conv/time_conv.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:responsive_builder/responsive_builder.dart'; 7 | import 'package:units_converter/units_converter.dart'; 8 | 9 | import '../../models/settings_model.dart'; 10 | import '../settings_page.dart'; 11 | 12 | class TimeConv extends StatefulWidget { 13 | const TimeConv({Key? key}) : super(key: key); 14 | static String pageTitle = "Time"; 15 | 16 | @override 17 | State createState() => _TimeConvState(); 18 | } 19 | 20 | class _TimeConvState extends State { 21 | TextEditingController inputA = TextEditingController(); 22 | FocusNode inputAFN = FocusNode(); 23 | TextEditingController inputB = TextEditingController(); 24 | FocusNode inputBFN = FocusNode(); 25 | 26 | var selectedtimeA; 27 | var selectedtimeB; 28 | var selectedtimeSymbolA; 29 | var selectedtimeSymbolB; 30 | var time = Time(significantFigures: 7, removeTrailingZeros: true); 31 | var units = []; 32 | 33 | void _bkspc() { 34 | if (inputAFN.hasFocus) { 35 | if (inputA.text.isNotEmpty) { 36 | if (inputA.selection.isCollapsed) { 37 | if (inputA.selection.baseOffset == inputA.text.length) { 38 | setState(() { 39 | inputA.text = inputA.text.substring(0, inputA.text.length - 1); 40 | }); 41 | inputA.selection = TextSelection.fromPosition( 42 | TextPosition(offset: inputA.text.length)); 43 | } else { 44 | setState(() { 45 | inputA.value = inputA.value.replaced( 46 | TextRange( 47 | start: inputA.selection.baseOffset - 1, 48 | end: inputA.selection.baseOffset), 49 | ""); 50 | }); 51 | inputA.selection = TextSelection.fromPosition( 52 | TextPosition(offset: inputA.selection.start)); 53 | } 54 | } else { 55 | setState(() { 56 | inputA.value = inputA.value.replaced( 57 | TextRange( 58 | start: inputA.selection.start, end: inputA.selection.end), 59 | ""); 60 | }); 61 | inputA.selection = TextSelection.fromPosition( 62 | TextPosition(offset: inputA.selection.end)); 63 | } 64 | if (inputA.text.isNotEmpty) { 65 | time.convert(selectedtimeA, double.parse(inputA.text)); 66 | 67 | units = time.getAll(); 68 | 69 | _convValueBuild(units); 70 | inputB.text = unitDetails[selectedtimeB] ?? ""; 71 | } else { 72 | setState(() { 73 | inputA.clear(); 74 | inputB.clear(); 75 | }); 76 | } 77 | } 78 | } else if (inputBFN.hasFocus) { 79 | if (inputB.text.isNotEmpty) { 80 | if (inputB.selection.isCollapsed) { 81 | if (inputB.selection.baseOffset == inputB.text.length) { 82 | setState(() { 83 | inputB.text = inputB.text.substring(0, inputB.text.length - 1); 84 | }); 85 | inputB.selection = TextSelection.fromPosition( 86 | TextPosition(offset: inputB.text.length)); 87 | } else { 88 | setState(() { 89 | inputB.value = inputB.value.replaced( 90 | TextRange( 91 | start: inputB.selection.baseOffset - 1, 92 | end: inputB.selection.baseOffset), 93 | ""); 94 | }); 95 | inputB.selection = TextSelection.fromPosition( 96 | TextPosition(offset: inputB.selection.start)); 97 | } 98 | } else { 99 | setState(() { 100 | inputB.value = inputB.value.replaced( 101 | TextRange( 102 | start: inputB.selection.start, end: inputB.selection.end), 103 | ""); 104 | }); 105 | inputB.selection = TextSelection.fromPosition( 106 | TextPosition(offset: inputB.selection.end)); 107 | } 108 | if (inputB.text.isNotEmpty) { 109 | time.convert(selectedtimeB, double.parse(inputB.text)); 110 | units = time.getAll(); 111 | 112 | _convValueBuild(units); 113 | inputA.text = unitDetails[selectedtimeA] ?? ""; 114 | } else { 115 | setState(() { 116 | inputA.clear(); 117 | inputB.clear(); 118 | }); 119 | } 120 | } 121 | } 122 | } 123 | 124 | Map unitDetails = {}; 125 | 126 | void _convValueBuild(unitsconv) { 127 | for (Unit unit in unitsconv) { 128 | if (unit.name == selectedtimeA) { 129 | setState(() { 130 | selectedtimeSymbolA = unit.symbol; 131 | }); 132 | } else if (unit.name == selectedtimeB) { 133 | setState(() { 134 | selectedtimeSymbolB = unit.symbol; 135 | }); 136 | } 137 | unitDetails.addAll({unit.name: unit.stringValue ?? ""}); 138 | } 139 | } 140 | 141 | void _conv(selectedUnit, val, TextEditingController input) { 142 | time.convert(selectedUnit, val); 143 | 144 | units = time.getAll(); 145 | 146 | _convValueBuild(units); 147 | input.text = unitDetails[selectedUnit] ?? ""; 148 | } 149 | 150 | void _convFunc(val) { 151 | if (val == "C") { 152 | inputA.clear(); 153 | inputB.clear(); 154 | } else { 155 | setState(() { 156 | if (inputAFN.hasFocus) { 157 | inputA.value = TextEditingValue( 158 | text: inputA.text.replaceRange( 159 | inputA.selection.start, inputA.selection.end, val), 160 | selection: TextSelection.collapsed( 161 | offset: inputA.selection.baseOffset + val.toString().length), 162 | ); 163 | time.convert(selectedtimeA, double.parse(inputA.text)); 164 | 165 | units = time.getAll(); 166 | 167 | _convValueBuild(units); 168 | inputB.text = unitDetails[selectedtimeB] ?? ""; 169 | } else if (inputBFN.hasFocus) { 170 | inputB.value = TextEditingValue( 171 | text: inputB.text.replaceRange( 172 | inputB.selection.start, inputB.selection.end, val), 173 | selection: TextSelection.collapsed( 174 | offset: inputB.selection.baseOffset + val.toString().length), 175 | ); 176 | time.convert(selectedtimeB, double.parse(inputB.text)); 177 | 178 | units = time.getAll(); 179 | 180 | _convValueBuild(units); 181 | inputA.text = unitDetails[selectedtimeA] ?? ""; 182 | } 183 | }); 184 | } 185 | } 186 | 187 | @override 188 | void initState() { 189 | super.initState(); 190 | setState(() { 191 | units = time.getAll(); 192 | selectedtimeA = TIME.minutes; 193 | selectedtimeB = TIME.seconds; 194 | }); 195 | inputAFN.requestFocus(); 196 | _convValueBuild(units); 197 | } 198 | 199 | @override 200 | Widget build(BuildContext context) { 201 | SettingsModel settings = Provider.of(context); 202 | time.significantFigures = settings.sigFig; 203 | return Scaffold( 204 | resizeToAvoidBottomInset: false, 205 | body: SafeArea( 206 | child: ResponsiveBuilder( 207 | builder: (context, sizingInformation) { 208 | if (sizingInformation.deviceScreenType == DeviceScreenType.tablet) { 209 | return OrientationBuilder( 210 | builder: (context, orientation) { 211 | if (orientation == Orientation.landscape) { 212 | return Row( 213 | crossAxisAlignment: CrossAxisAlignment.center, 214 | mainAxisAlignment: MainAxisAlignment.center, 215 | children: [ 216 | Expanded( 217 | child: _inputView(context, 48), 218 | ), 219 | Expanded(child: _keypad(context, 1.42)) 220 | ], 221 | ); 222 | } else { 223 | return Column( 224 | crossAxisAlignment: CrossAxisAlignment.center, 225 | mainAxisAlignment: MainAxisAlignment.center, 226 | children: [ 227 | Expanded( 228 | child: _inputView(context, 48), 229 | ), 230 | _keypad(context, 2) 231 | ], 232 | ); 233 | } 234 | }, 235 | ); 236 | } 237 | return OrientationBuilder( 238 | builder: (context, orientation) { 239 | if (orientation == Orientation.landscape) { 240 | return Row( 241 | children: [ 242 | Expanded(child: _inputView(context, 32)), 243 | Expanded(child: _keypad(context, 2.4)), 244 | ], 245 | ); 246 | } else { 247 | return Column( 248 | children: [ 249 | Expanded( 250 | flex: 1, 251 | child: _inputView(context, 48), 252 | ), 253 | _keypad(context, 1.8) 254 | ], 255 | ); 256 | } 257 | }, 258 | ); 259 | }, 260 | ), 261 | ), 262 | ); 263 | } 264 | 265 | Widget _keypad(BuildContext context, double cellSizeRatio) { 266 | return GridView( 267 | shrinkWrap: true, 268 | padding: const EdgeInsets.all(8), 269 | physics: const NeverScrollableScrollPhysics(), 270 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 271 | crossAxisCount: 3, 272 | crossAxisSpacing: 8, 273 | mainAxisSpacing: 8, 274 | childAspectRatio: cellSizeRatio), 275 | children: [ 276 | FilledButton( 277 | onPressed: () { 278 | if (inputBFN.hasFocus) { 279 | inputBFN.unfocus(); 280 | inputAFN.requestFocus(); 281 | } else if (inputAFN.hasFocus) { 282 | inputAFN.unfocus(); 283 | inputBFN.requestFocus(); 284 | } 285 | }, 286 | child: Transform.rotate( 287 | angle: 90 * pi / 180, 288 | child: const Icon( 289 | Icons.compare_arrows, 290 | size: 32, 291 | ), 292 | )), 293 | _buildButtons("C", false), 294 | FilledButton( 295 | onPressed: () { 296 | _bkspc(); 297 | 298 | HapticFeedback.lightImpact(); 299 | }, 300 | child: const Icon( 301 | Icons.backspace_outlined, 302 | size: 32, 303 | )), 304 | _buildButtons("7", true), 305 | _buildButtons("8", true), 306 | _buildButtons("9", true), 307 | _buildButtons("4", true), 308 | _buildButtons("5", true), 309 | _buildButtons("6", true), 310 | _buildButtons("1", true), 311 | _buildButtons("2", true), 312 | _buildButtons("3", true), 313 | const FilledButton.tonal( 314 | onPressed: null, 315 | child: Text( 316 | "\u00b1", 317 | style: TextStyle( 318 | fontSize: 32, 319 | ), 320 | )), 321 | _buildButtons("0", true), 322 | _buildButtons(".", true), 323 | ], 324 | ); 325 | } 326 | 327 | Widget _inputView(BuildContext context, double fontsize) { 328 | return Container( 329 | decoration: BoxDecoration( 330 | borderRadius: BorderRadius.circular(16), 331 | color: Theme.of(context).colorScheme.secondaryContainer, 332 | ), 333 | padding: const EdgeInsets.all(8), 334 | margin: const EdgeInsets.all(8), 335 | child: Column( 336 | crossAxisAlignment: CrossAxisAlignment.start, 337 | mainAxisAlignment: MainAxisAlignment.start, 338 | children: [ 339 | DropdownMenu( 340 | dropdownMenuEntries: List.generate( 341 | units.length, 342 | growable: false, 343 | (index) { 344 | var unit = units[index]; 345 | return DropdownMenuEntry( 346 | value: unit.name, 347 | label: unit.name.toString().split("TIME.").last.capitalize(), 348 | ); 349 | }, 350 | ), 351 | initialSelection: selectedtimeA, 352 | onSelected: (value) { 353 | setState(() { 354 | selectedtimeA = value; 355 | }); 356 | if (inputAFN.hasFocus) { 357 | if (inputA.text.isNotEmpty) { 358 | time.convert(value, double.parse(inputA.text)); 359 | units = time.getAll(); 360 | 361 | _convValueBuild(units); 362 | inputB.text = unitDetails[selectedtimeB] ?? ""; 363 | } 364 | } else if (inputBFN.hasFocus) { 365 | if (inputB.text.isNotEmpty) { 366 | time.convert(selectedtimeB, double.parse(inputB.text)); 367 | units = time.getAll(); 368 | 369 | _convValueBuild(units); 370 | inputA.text = unitDetails[value] ?? ""; 371 | } 372 | } 373 | }, 374 | ), 375 | TextField( 376 | enableSuggestions: false, 377 | textAlign: TextAlign.right, 378 | decoration: InputDecoration( 379 | border: InputBorder.none, 380 | suffixText: selectedtimeSymbolA.toString(), 381 | ), 382 | controller: inputA, 383 | focusNode: inputAFN, 384 | onChanged: (value) { 385 | if (inputAFN.hasFocus) { 386 | _conv(selectedtimeA, value, inputB); 387 | } 388 | }, 389 | inputFormatters: [ 390 | FilteringTextInputFormatter.deny(RegExp(r'[a-z] [A-Z] :$')) 391 | ], 392 | style: TextStyle( 393 | fontSize: fontsize, 394 | ), 395 | keyboardType: TextInputType.none, 396 | ), 397 | const Divider(), 398 | DropdownMenu( 399 | dropdownMenuEntries: List.generate( 400 | units.length, 401 | growable: false, 402 | (index) { 403 | var unit = units[index]; 404 | return DropdownMenuEntry( 405 | value: unit.name, 406 | label: unit.name.toString().split("TIME.").last.capitalize(), 407 | ); 408 | }, 409 | ), 410 | initialSelection: selectedtimeB, 411 | onSelected: (value) { 412 | setState(() { 413 | selectedtimeB = value; 414 | }); 415 | if (inputBFN.hasFocus) { 416 | if (inputB.text.isNotEmpty) { 417 | time.convert(value, double.parse(inputB.text)); 418 | units = time.getAll(); 419 | 420 | _convValueBuild(units); 421 | inputA.text = unitDetails[selectedtimeA] ?? ""; 422 | } 423 | } else if (inputAFN.hasFocus) { 424 | if (inputA.text.isNotEmpty) { 425 | time.convert(selectedtimeA, double.parse(inputA.text)); 426 | units = time.getAll(); 427 | 428 | _convValueBuild(units); 429 | inputB.text = unitDetails[value] ?? ""; 430 | } 431 | } 432 | }, 433 | ), 434 | TextField( 435 | enableSuggestions: false, 436 | textAlign: TextAlign.right, 437 | decoration: InputDecoration( 438 | border: InputBorder.none, 439 | suffixText: selectedtimeSymbolB.toString(), 440 | ), 441 | controller: inputB, 442 | focusNode: inputBFN, 443 | onChanged: (value) { 444 | if (inputBFN.hasFocus) { 445 | _conv(selectedtimeB, value, inputA); 446 | } 447 | }, 448 | inputFormatters: [ 449 | FilteringTextInputFormatter.deny(RegExp(r'[a-z] [A-Z] :$')) 450 | ], 451 | style: TextStyle( 452 | fontSize: fontsize, 453 | ), 454 | keyboardType: TextInputType.none, 455 | ), 456 | ], 457 | ), 458 | ); 459 | } 460 | 461 | Widget _buildButtons(String label, bool tonal) { 462 | return tonal 463 | ? SizedBox( 464 | height: 32, 465 | width: 72, 466 | child: FilledButton.tonal( 467 | onPressed: () { 468 | _convFunc(label); 469 | HapticFeedback.lightImpact(); 470 | }, 471 | child: Text( 472 | label, 473 | style: const TextStyle( 474 | fontSize: 32, 475 | ), 476 | )), 477 | ) 478 | : SizedBox( 479 | height: 32, 480 | width: 72, 481 | child: FilledButton( 482 | onPressed: () { 483 | _convFunc(label); 484 | HapticFeedback.lightImpact(); 485 | }, 486 | child: Text( 487 | label, 488 | style: const TextStyle( 489 | fontSize: 32, 490 | ), 491 | )), 492 | ); 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /lib/pages/conv/volume_conv.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:responsive_builder/responsive_builder.dart'; 7 | import 'package:units_converter/units_converter.dart'; 8 | 9 | import '../../models/settings_model.dart'; 10 | import '../settings_page.dart'; 11 | 12 | class VolumeConv extends StatefulWidget { 13 | const VolumeConv({Key? key}) : super(key: key); 14 | static String pageTitle = "Volume"; 15 | 16 | @override 17 | State createState() => _VolumeConvState(); 18 | } 19 | 20 | class _VolumeConvState extends State { 21 | TextEditingController inputA = TextEditingController(); 22 | FocusNode inputAFN = FocusNode(); 23 | TextEditingController inputB = TextEditingController(); 24 | FocusNode inputBFN = FocusNode(); 25 | 26 | var selectedVolumeA; 27 | var selectedVolumeB; 28 | var selectedVolumeSymbolA; 29 | var selectedVolumeSymbolB; 30 | var volume = Volume(significantFigures: 7, removeTrailingZeros: true); 31 | var units = []; 32 | 33 | void _bkspc() { 34 | if (inputAFN.hasFocus) { 35 | if (inputA.text.isNotEmpty) { 36 | if (inputA.selection.isCollapsed) { 37 | if (inputA.selection.baseOffset == inputA.text.length) { 38 | setState(() { 39 | inputA.text = inputA.text.substring(0, inputA.text.length - 1); 40 | }); 41 | inputA.selection = TextSelection.fromPosition( 42 | TextPosition(offset: inputA.text.length)); 43 | } else { 44 | setState(() { 45 | inputA.value = inputA.value.replaced( 46 | TextRange( 47 | start: inputA.selection.baseOffset - 1, 48 | end: inputA.selection.baseOffset), 49 | ""); 50 | }); 51 | inputA.selection = TextSelection.fromPosition( 52 | TextPosition(offset: inputA.selection.start)); 53 | } 54 | } else { 55 | setState(() { 56 | inputA.value = inputA.value.replaced( 57 | TextRange( 58 | start: inputA.selection.start, end: inputA.selection.end), 59 | ""); 60 | }); 61 | inputA.selection = TextSelection.fromPosition( 62 | TextPosition(offset: inputA.selection.end)); 63 | } 64 | if (inputA.text.isNotEmpty) { 65 | volume.convert(selectedVolumeA, double.parse(inputA.text)); 66 | 67 | units = volume.getAll(); 68 | 69 | _convValueBuild(units); 70 | inputB.text = unitDetails[selectedVolumeB] ?? ""; 71 | } else { 72 | setState(() { 73 | inputA.clear(); 74 | inputB.clear(); 75 | }); 76 | } 77 | } 78 | } else if (inputBFN.hasFocus) { 79 | if (inputB.text.isNotEmpty) { 80 | if (inputB.selection.isCollapsed) { 81 | if (inputB.selection.baseOffset == inputB.text.length) { 82 | setState(() { 83 | inputB.text = inputB.text.substring(0, inputB.text.length - 1); 84 | }); 85 | inputB.selection = TextSelection.fromPosition( 86 | TextPosition(offset: inputB.text.length)); 87 | } else { 88 | setState(() { 89 | inputB.value = inputB.value.replaced( 90 | TextRange( 91 | start: inputB.selection.baseOffset - 1, 92 | end: inputB.selection.baseOffset), 93 | ""); 94 | }); 95 | inputB.selection = TextSelection.fromPosition( 96 | TextPosition(offset: inputB.selection.start)); 97 | } 98 | } else { 99 | setState(() { 100 | inputB.value = inputB.value.replaced( 101 | TextRange( 102 | start: inputB.selection.start, end: inputB.selection.end), 103 | ""); 104 | }); 105 | inputB.selection = TextSelection.fromPosition( 106 | TextPosition(offset: inputB.selection.end)); 107 | } 108 | if (inputB.text.isNotEmpty) { 109 | volume.convert(selectedVolumeB, double.parse(inputB.text)); 110 | units = volume.getAll(); 111 | 112 | _convValueBuild(units); 113 | inputA.text = unitDetails[selectedVolumeA] ?? ""; 114 | } else { 115 | setState(() { 116 | inputA.clear(); 117 | inputB.clear(); 118 | }); 119 | } 120 | } 121 | } 122 | } 123 | 124 | Map unitDetails = {}; 125 | 126 | void _convValueBuild(unitsconv) { 127 | for (Unit unit in unitsconv) { 128 | if (unit.name == selectedVolumeA) { 129 | setState(() { 130 | selectedVolumeSymbolA = unit.symbol; 131 | }); 132 | } else if (unit.name == selectedVolumeB) { 133 | setState(() { 134 | selectedVolumeSymbolB = unit.symbol; 135 | }); 136 | } 137 | unitDetails.addAll({unit.name: unit.stringValue ?? ""}); 138 | } 139 | } 140 | 141 | void _conv(selectedUnit, val, TextEditingController input) { 142 | volume.convert(selectedUnit, val); 143 | 144 | units = volume.getAll(); 145 | 146 | _convValueBuild(units); 147 | input.text = unitDetails[selectedUnit] ?? ""; 148 | } 149 | 150 | void _convFunc(val) { 151 | if (val == "C") { 152 | inputA.clear(); 153 | inputB.clear(); 154 | } else { 155 | setState(() { 156 | if (inputAFN.hasFocus) { 157 | inputA.value = TextEditingValue( 158 | text: inputA.text.replaceRange( 159 | inputA.selection.start, inputA.selection.end, val), 160 | selection: TextSelection.collapsed( 161 | offset: inputA.selection.baseOffset + val.toString().length), 162 | ); 163 | volume.convert(selectedVolumeA, double.parse(inputA.text)); 164 | 165 | units = volume.getAll(); 166 | 167 | _convValueBuild(units); 168 | inputB.text = unitDetails[selectedVolumeB] ?? ""; 169 | } else if (inputBFN.hasFocus) { 170 | inputB.value = TextEditingValue( 171 | text: inputB.text.replaceRange( 172 | inputB.selection.start, inputB.selection.end, val), 173 | selection: TextSelection.collapsed( 174 | offset: inputB.selection.baseOffset + val.toString().length), 175 | ); 176 | volume.convert(selectedVolumeB, double.parse(inputB.text)); 177 | 178 | units = volume.getAll(); 179 | 180 | _convValueBuild(units); 181 | inputA.text = unitDetails[selectedVolumeA] ?? ""; 182 | } 183 | }); 184 | } 185 | } 186 | 187 | @override 188 | void initState() { 189 | super.initState(); 190 | setState(() { 191 | units = volume.getAll(); 192 | selectedVolumeA = VOLUME.liters; 193 | selectedVolumeB = VOLUME.milliliters; 194 | }); 195 | inputAFN.requestFocus(); 196 | _convValueBuild(units); 197 | } 198 | 199 | @override 200 | Widget build(BuildContext context) { 201 | SettingsModel settings = Provider.of(context); 202 | volume.significantFigures = settings.sigFig; 203 | return Scaffold( 204 | resizeToAvoidBottomInset: false, 205 | body: SafeArea( 206 | child: ResponsiveBuilder( 207 | builder: (context, sizingInformation) { 208 | if (sizingInformation.deviceScreenType == DeviceScreenType.tablet) { 209 | return OrientationBuilder( 210 | builder: (context, orientation) { 211 | if (orientation == Orientation.landscape) { 212 | return Row( 213 | crossAxisAlignment: CrossAxisAlignment.center, 214 | mainAxisAlignment: MainAxisAlignment.center, 215 | children: [ 216 | Expanded( 217 | child: _inputView(context, 48), 218 | ), 219 | Expanded(child: _keypad(context, 1.42)) 220 | ], 221 | ); 222 | } else { 223 | return Column( 224 | crossAxisAlignment: CrossAxisAlignment.center, 225 | mainAxisAlignment: MainAxisAlignment.center, 226 | children: [ 227 | Expanded( 228 | child: _inputView(context, 48), 229 | ), 230 | _keypad(context, 2) 231 | ], 232 | ); 233 | } 234 | }, 235 | ); 236 | } 237 | return OrientationBuilder( 238 | builder: (context, orientation) { 239 | if (orientation == Orientation.landscape) { 240 | return Row( 241 | children: [ 242 | Expanded(child: _inputView(context, 32)), 243 | Expanded(child: _keypad(context, 2.4)), 244 | ], 245 | ); 246 | } else { 247 | return Column( 248 | children: [ 249 | Expanded( 250 | flex: 1, 251 | child: _inputView(context, 48), 252 | ), 253 | _keypad(context, 1.8) 254 | ], 255 | ); 256 | } 257 | }, 258 | ); 259 | }, 260 | ), 261 | ), 262 | ); 263 | } 264 | 265 | Widget _keypad(BuildContext context, double cellSizeRatio) { 266 | return GridView( 267 | shrinkWrap: true, 268 | padding: const EdgeInsets.all(8), 269 | physics: const NeverScrollableScrollPhysics(), 270 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 271 | crossAxisCount: 3, 272 | crossAxisSpacing: 8, 273 | mainAxisSpacing: 8, 274 | childAspectRatio: cellSizeRatio), 275 | children: [ 276 | FilledButton( 277 | onPressed: () { 278 | if (inputBFN.hasFocus) { 279 | inputBFN.unfocus(); 280 | inputAFN.requestFocus(); 281 | } else if (inputAFN.hasFocus) { 282 | inputAFN.unfocus(); 283 | inputBFN.requestFocus(); 284 | } 285 | }, 286 | child: Transform.rotate( 287 | angle: 90 * pi / 180, 288 | child: const Icon( 289 | Icons.compare_arrows, 290 | size: 32, 291 | ), 292 | )), 293 | _buildButtons("C", false), 294 | FilledButton( 295 | onPressed: () { 296 | _bkspc(); 297 | 298 | HapticFeedback.lightImpact(); 299 | }, 300 | child: const Icon( 301 | Icons.backspace_outlined, 302 | size: 32, 303 | )), 304 | _buildButtons("7", true), 305 | _buildButtons("8", true), 306 | _buildButtons("9", true), 307 | _buildButtons("4", true), 308 | _buildButtons("5", true), 309 | _buildButtons("6", true), 310 | _buildButtons("1", true), 311 | _buildButtons("2", true), 312 | _buildButtons("3", true), 313 | const FilledButton.tonal( 314 | onPressed: null, 315 | child: Text( 316 | "\u00b1", 317 | style: TextStyle( 318 | fontSize: 32, 319 | ), 320 | )), 321 | _buildButtons("0", true), 322 | _buildButtons(".", true), 323 | ], 324 | ); 325 | } 326 | 327 | Widget _inputView(BuildContext context, double fontsize) { 328 | return Container( 329 | decoration: BoxDecoration( 330 | borderRadius: BorderRadius.circular(16), 331 | color: Theme.of(context).colorScheme.secondaryContainer, 332 | ), 333 | padding: const EdgeInsets.all(8), 334 | margin: const EdgeInsets.all(8), 335 | child: Column( 336 | crossAxisAlignment: CrossAxisAlignment.start, 337 | mainAxisAlignment: MainAxisAlignment.start, 338 | children: [ 339 | DropdownMenu( 340 | dropdownMenuEntries: List.generate( 341 | units.length, 342 | growable: false, 343 | (index) { 344 | var unit = units[index]; 345 | return DropdownMenuEntry( 346 | value: unit.name, 347 | label: 348 | unit.name.toString().split("VOLUME.").last.capitalize(), 349 | ); 350 | }, 351 | ), 352 | initialSelection: selectedVolumeA, 353 | onSelected: (value) { 354 | setState(() { 355 | selectedVolumeA = value; 356 | }); 357 | if (inputAFN.hasFocus) { 358 | if (inputA.text.isNotEmpty) { 359 | volume.convert(value, double.parse(inputA.text)); 360 | units = volume.getAll(); 361 | 362 | _convValueBuild(units); 363 | inputB.text = unitDetails[selectedVolumeB] ?? ""; 364 | } 365 | } else if (inputBFN.hasFocus) { 366 | if (inputB.text.isNotEmpty) { 367 | volume.convert(selectedVolumeB, double.parse(inputB.text)); 368 | units = volume.getAll(); 369 | 370 | _convValueBuild(units); 371 | inputA.text = unitDetails[value] ?? ""; 372 | } 373 | } 374 | }, 375 | ), 376 | TextField( 377 | enableSuggestions: false, 378 | textAlign: TextAlign.right, 379 | decoration: InputDecoration( 380 | border: InputBorder.none, 381 | suffixText: selectedVolumeSymbolA.toString(), 382 | ), 383 | controller: inputA, 384 | focusNode: inputAFN, 385 | onChanged: (value) { 386 | if (inputAFN.hasFocus) { 387 | _conv(selectedVolumeA, value, inputB); 388 | } 389 | }, 390 | inputFormatters: [ 391 | FilteringTextInputFormatter.deny(RegExp(r'[a-z] [A-Z] :$')) 392 | ], 393 | style: TextStyle( 394 | fontSize: fontsize, 395 | ), 396 | keyboardType: TextInputType.none, 397 | ), 398 | const Divider(), 399 | DropdownMenu( 400 | dropdownMenuEntries: List.generate( 401 | units.length, 402 | growable: false, 403 | (index) { 404 | var unit = units[index]; 405 | return DropdownMenuEntry( 406 | value: unit.name, 407 | label: 408 | unit.name.toString().split("VOLUME.").last.capitalize(), 409 | ); 410 | }, 411 | ), 412 | initialSelection: selectedVolumeB, 413 | onSelected: (value) { 414 | setState(() { 415 | selectedVolumeB = value; 416 | }); 417 | if (inputBFN.hasFocus) { 418 | if (inputB.text.isNotEmpty) { 419 | volume.convert(value, double.parse(inputB.text)); 420 | units = volume.getAll(); 421 | 422 | _convValueBuild(units); 423 | inputA.text = unitDetails[selectedVolumeA] ?? ""; 424 | } 425 | } else if (inputAFN.hasFocus) { 426 | if (inputA.text.isNotEmpty) { 427 | volume.convert(selectedVolumeA, double.parse(inputA.text)); 428 | units = volume.getAll(); 429 | 430 | _convValueBuild(units); 431 | inputB.text = unitDetails[value] ?? ""; 432 | } 433 | } 434 | }, 435 | ), 436 | TextField( 437 | enableSuggestions: false, 438 | textAlign: TextAlign.right, 439 | decoration: InputDecoration( 440 | border: InputBorder.none, 441 | suffixText: selectedVolumeSymbolB.toString(), 442 | ), 443 | controller: inputB, 444 | focusNode: inputBFN, 445 | onChanged: (value) { 446 | if (inputBFN.hasFocus) { 447 | _conv(selectedVolumeB, value, inputA); 448 | } 449 | }, 450 | inputFormatters: [ 451 | FilteringTextInputFormatter.deny(RegExp(r'[a-z] [A-Z] :$')) 452 | ], 453 | style: TextStyle( 454 | fontSize: fontsize, 455 | ), 456 | keyboardType: TextInputType.none, 457 | ), 458 | ], 459 | ), 460 | ); 461 | } 462 | 463 | Widget _buildButtons(String label, bool tonal) { 464 | return tonal 465 | ? SizedBox( 466 | height: 32, 467 | width: 72, 468 | child: FilledButton.tonal( 469 | onPressed: () { 470 | _convFunc(label); 471 | HapticFeedback.lightImpact(); 472 | }, 473 | child: Text( 474 | label, 475 | style: const TextStyle( 476 | fontSize: 32, 477 | ), 478 | )), 479 | ) 480 | : SizedBox( 481 | height: 32, 482 | width: 72, 483 | child: FilledButton( 484 | onPressed: () { 485 | _convFunc(label); 486 | HapticFeedback.lightImpact(); 487 | }, 488 | child: Text( 489 | label, 490 | style: const TextStyle( 491 | fontSize: 32, 492 | ), 493 | )), 494 | ); 495 | } 496 | } 497 | -------------------------------------------------------------------------------- /lib/pages/pages.dart: -------------------------------------------------------------------------------- 1 | //Calculator Pages 2 | export './calc/standard_calc.dart'; 3 | export './calc/scientific_calc.dart'; 4 | export './calc/date_calc.dart'; 5 | 6 | //Converter Pages 7 | export './conv/angle_conv.dart'; 8 | export './conv/area_conv.dart'; 9 | export './conv/data_conv.dart'; 10 | export './conv/energy_conv.dart'; 11 | export './conv/length_conv.dart'; 12 | export './conv/mass_conv.dart'; 13 | export './conv/power_conv.dart'; 14 | export './conv/pressure_conv.dart'; 15 | export './conv/speed_conv.dart'; 16 | export './conv/temp_conv.dart'; 17 | export './conv/time_conv.dart'; 18 | export './conv/volume_conv.dart'; 19 | 20 | //General 21 | export './settings_page.dart'; 22 | -------------------------------------------------------------------------------- /lib/pages/settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:animations/animations.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:flutter_svg/flutter_svg.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:url_launcher/url_launcher.dart'; 7 | 8 | import '../models/settings_model.dart'; 9 | 10 | class SettingsPage extends StatelessWidget { 11 | SettingsPage({super.key}); 12 | final TextEditingController sigFigInput = TextEditingController(); 13 | Route _createRoute(Widget widget) { 14 | return PageRouteBuilder( 15 | pageBuilder: (context, animation, secondaryAnimation) => widget, 16 | transitionsBuilder: (context, animation, secondaryAnimation, child) { 17 | return SharedAxisTransition( 18 | animation: animation, 19 | secondaryAnimation: secondaryAnimation, 20 | transitionType: SharedAxisTransitionType.horizontal, 21 | child: child, 22 | ); 23 | }, 24 | ); 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | SettingsModel settings = Provider.of(context); 30 | return Scaffold( 31 | body: CustomScrollView( 32 | slivers: [ 33 | SliverAppBar.large( 34 | title: const Text("Settings"), 35 | ), 36 | SliverToBoxAdapter( 37 | child: ListView( 38 | shrinkWrap: true, 39 | physics: const NeverScrollableScrollPhysics(), 40 | children: [ 41 | ListTile( 42 | leading: const Icon(Icons.color_lens_outlined), 43 | title: const Text("Theme"), 44 | onTap: () => 45 | Navigator.push(context, _createRoute(const ThemePage())), 46 | ), 47 | ListTile( 48 | leading: const Icon(Icons.exposure_zero_outlined), 49 | title: const Text("Set Significant Figures"), 50 | onTap: () => showDialog( 51 | context: context, 52 | builder: (context) { 53 | sigFigInput.text = settings.sigFig.toString(); 54 | return Padding( 55 | padding: const EdgeInsets.all(16.0), 56 | child: AlertDialog( 57 | title: Text( 58 | "Set significant figures", 59 | style: Theme.of(context).textTheme.headlineMedium, 60 | ), 61 | content: Column( 62 | mainAxisSize: MainAxisSize.min, 63 | children: [ 64 | const Text( 65 | "This settings is exclusively for unit convertors"), 66 | TextField( 67 | textAlign: TextAlign.end, 68 | keyboardType: TextInputType.number, 69 | controller: sigFigInput, 70 | onChanged: (value) => value.isNotEmpty 71 | ? settings.sigFig = int.parse(value) 72 | : settings.sigFig = 7, 73 | ), 74 | ], 75 | ), 76 | actions: [ 77 | TextButton( 78 | onPressed: () { 79 | Navigator.pop(context); 80 | }, 81 | child: const Text("Cancel"), 82 | ), 83 | TextButton( 84 | onPressed: () { 85 | settings.sigFig = int.parse(sigFigInput.text); 86 | Navigator.pop(context); 87 | }, 88 | child: const Text("Set"), 89 | ), 90 | ], 91 | ), 92 | ); 93 | }), 94 | ), 95 | ListTile( 96 | leading: const Icon(Icons.info_outline), 97 | title: const Text("About"), 98 | onTap: () => 99 | Navigator.push(context, _createRoute(const About())), 100 | ) 101 | ], 102 | ), 103 | ) 104 | ], 105 | ), 106 | ); 107 | } 108 | } 109 | 110 | class ThemePage extends StatefulWidget { 111 | const ThemePage({super.key}); 112 | 113 | @override 114 | State createState() => _ThemePageState(); 115 | } 116 | 117 | class _ThemePageState extends State { 118 | static const platform = 119 | MethodChannel('bored.codebyk.mintcalc/androidversion'); 120 | 121 | int av = 0; 122 | Future androidVersion() async { 123 | final result = await platform.invokeMethod('getAndroidVersion'); 124 | return await result; 125 | } 126 | 127 | void fetchVersion() async { 128 | final v = await androidVersion(); 129 | setState(() { 130 | av = v; 131 | }); 132 | } 133 | 134 | @override 135 | void initState() { 136 | super.initState(); 137 | fetchVersion(); 138 | } 139 | 140 | @override 141 | Widget build(BuildContext context) { 142 | SettingsModel settings = Provider.of(context); 143 | return Scaffold( 144 | body: CustomScrollView( 145 | slivers: [ 146 | SliverAppBar.large( 147 | title: const Text("Theme"), 148 | ), 149 | SliverToBoxAdapter( 150 | child: Column( 151 | crossAxisAlignment: CrossAxisAlignment.stretch, 152 | mainAxisAlignment: MainAxisAlignment.start, 153 | children: [ 154 | Padding( 155 | padding: const EdgeInsets.all(16.0), 156 | child: SegmentedButton( 157 | segments: const [ 158 | ButtonSegment( 159 | value: ThemeMode.system, label: Text("System")), 160 | ButtonSegment( 161 | value: ThemeMode.light, label: Text("Light")), 162 | ButtonSegment(value: ThemeMode.dark, label: Text("Dark")), 163 | ], 164 | selected: {settings.themeMode}, 165 | onSelectionChanged: (p0) { 166 | settings.themeMode = p0.first; 167 | }, 168 | ), 169 | ), 170 | ListView( 171 | shrinkWrap: true, 172 | children: [ 173 | SwitchListTile( 174 | value: settings.isSystemColor, 175 | onChanged: av >= 31 176 | ? (value) => settings.isSystemColor = value 177 | : null, 178 | title: const Text("Use system color scheme"), 179 | subtitle: Text(settings.isSystemColor 180 | ? "Using system dynamic color" 181 | : "Using default color scheme"), 182 | ), 183 | ], 184 | ) 185 | ], 186 | ), 187 | ) 188 | ], 189 | ), 190 | ); 191 | } 192 | } 193 | 194 | class About extends StatefulWidget { 195 | const About({super.key}); 196 | 197 | @override 198 | State createState() => _AboutState(); 199 | } 200 | 201 | class _AboutState extends State { 202 | @override 203 | void initState() { 204 | super.initState(); 205 | } 206 | 207 | @override 208 | Widget build(BuildContext context) { 209 | return Scaffold( 210 | body: CustomScrollView(slivers: [ 211 | SliverAppBar.large( 212 | title: const Text("About"), 213 | ), 214 | SliverToBoxAdapter( 215 | child: ListView( 216 | shrinkWrap: true, 217 | physics: const NeverScrollableScrollPhysics(), 218 | children: [ 219 | const ListTile( 220 | leading: Icon(Icons.info_outline), 221 | title: Text("App Version"), 222 | subtitle: Text("1.1.0"), 223 | ), 224 | ListTile( 225 | leading: const Icon(Icons.info_outline), 226 | title: const Text("Licenses"), 227 | onTap: () => showLicensePage( 228 | context: context, 229 | applicationName: "Mint Calculator", 230 | applicationVersion: "1.1.0"), 231 | ), 232 | ListTile( 233 | leading: SvgPicture.asset( 234 | Theme.of(context).brightness == Brightness.light 235 | ? "assets/github-mark.svg" 236 | : "assets/github-mark-white.svg", 237 | semanticsLabel: 'Github', 238 | height: 24, 239 | width: 24, 240 | ), 241 | title: const Text("Github"), 242 | onTap: () async { 243 | const url = 'https://github.com/boredcodebyk/mintcalc'; 244 | if (!await launchUrl(Uri.parse(url), 245 | mode: LaunchMode.externalApplication)) { 246 | throw Exception('Could not launch $url'); 247 | } 248 | }, 249 | ), 250 | ], 251 | ), 252 | ), 253 | ]), 254 | ); 255 | } 256 | } 257 | 258 | extension StringExtension on String { 259 | /// Capitalize the first letter of a word 260 | String capitalize() { 261 | return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/100.txt: -------------------------------------------------------------------------------- 1 | First release -------------------------------------------------------------------------------- /metadata/en-US/changelogs/110.txt: -------------------------------------------------------------------------------- 1 | - Bug fixes 2 | - Optimized for landscape orientation and larger device -------------------------------------------------------------------------------- /metadata/en-US/changelogs/111.txt: -------------------------------------------------------------------------------- 1 | - Added and fixed haptic feedback -------------------------------------------------------------------------------- /metadata/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | A simple calculator and unit converter app with Material Design 3 inspired by Windows Calculator 2 | 3 | Features 4 | - Standard Calculator 5 | - Calculator history 6 | - Date Calculator 7 | - Simple unit converter (Angle, Time, Data, Length, Area, Volume, etc...) 8 | 9 | Planned updates 10 | - Scientific calculator -------------------------------------------------------------------------------- /metadata/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/metadata/en-US/images/icon.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/metadata/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/metadata/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boredcodebyk/mintcalc/2559a8bb8e1a444e267343b86e47d8033a139e02/metadata/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /metadata/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | A simple calculator and converter app with Material Design 3 -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: mintcalc 2 | description: A simple calculator and unit converter app with Material Design 3 3 | inspired by Windows Calculator. 4 | 5 | publish_to: 'none' 6 | 7 | version: 1.1.1+111 8 | 9 | environment: 10 | sdk: '>=3.0.5 <4.0.0' 11 | 12 | dependencies: 13 | animations: ^2.0.7 14 | cupertino_icons: ^1.0.2 15 | duration: ^3.0.12 16 | dynamic_color: ^1.6.5 17 | flutter: 18 | sdk: flutter 19 | flutter_svg: ^2.0.7 20 | intl: ^0.18.1 21 | math_expressions: ^2.4.0 22 | provider: ^6.0.5 23 | responsive_builder: ^0.7.0 24 | shared_preferences: ^2.2.0 25 | units_converter: ^2.1.0 26 | url_launcher: ^6.1.11 27 | 28 | dev_dependencies: 29 | build_runner: ^2.4.5 30 | flutter_lints: ^2.0.0 31 | flutter_test: 32 | sdk: flutter 33 | 34 | flutter: 35 | 36 | uses-material-design: true 37 | assets: 38 | - assets/github-mark-white.svg 39 | - assets/github-mark.svg 40 | fonts: 41 | - family: Manrope 42 | fonts: 43 | - asset: fonts/Manrope/Manrope-Regular.ttf 44 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:mintcalc/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | --------------------------------------------------------------------------------