├── .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 | 
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 | [
](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 | | | |
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 |
--------------------------------------------------------------------------------