diff --git a/.github/workflows/all_packages.yml b/.github/workflows/all_packages.yml index 9716b42e2..ab7f22858 100644 --- a/.github/workflows/all_packages.yml +++ b/.github/workflows/all_packages.yml @@ -26,7 +26,7 @@ jobs: - name: Init workspace uses: ./.github/workflows/init-workspace - name: Run analyze - run: melos run analyze + run: melos analyze format: runs-on: ubuntu-latest @@ -38,7 +38,7 @@ jobs: - name: Init workspace uses: ./.github/workflows/init-workspace - name: Run format - run: melos run format:ci + run: melos format --set-exit-if-changed test: runs-on: ubuntu-latest diff --git a/.github/workflows/init-workspace/action.yml b/.github/workflows/init-workspace/action.yml index 5b4d32b82..565c0c76e 100644 --- a/.github/workflows/init-workspace/action.yml +++ b/.github/workflows/init-workspace/action.yml @@ -10,7 +10,7 @@ runs: with: channel: stable # Increase this version manually when we add support for a new Flutter release - flutter-version: '3.19.6' + flutter-version: '3.22.x' - name: Install melos run: dart pub global activate melos diff --git a/CHANGELOG.md b/CHANGELOG.md index 23f3fc9b7..1dcb3ad06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,129 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 2024-06-12 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`studyu_app` - `v2.7.6-dev.2`](#studyu_app---v276-dev2) + - [`studyu_core` - `v4.4.3-dev.1`](#studyu_core---v443-dev1) + - [`studyu_designer_v2` - `v1.8.1-dev.2`](#studyu_designer_v2---v181-dev2) + - [`studyu_flutter_common` - `v1.8.4-dev.1`](#studyu_flutter_common---v184-dev1) + +--- + +#### `studyu_app` - `v2.7.6-dev.2` + + - **FIX**: formatting issues. + - **FIX**(app): update ios deployment. + - **FIX**(app): update android deployment. + - **FIX**: upgrade deps. + - **FIX**(app): update ios deployment. + - **FIX**(app): update android deployment. + - **FIX**(app): update android deployment. + - **FIX**(app): update ios deployment. + - **FIX**(app): update android deployment. + +#### `studyu_core` - `v4.4.3-dev.1` + + - **PERF**: improve dashboard study fetching. + - **FIX**: formatting issues. + - **FIX**: upgrade deps. + +#### `studyu_designer_v2` - `v1.8.1-dev.2` + + - **PERF**: added comments to the getUserStudies function. + - **PERF**: improve dashboard study fetching. + - **FIX**: add emojis again. + - **FIX**: integration test sign out. + - **FIX**: check if canPop. + - **FIX**: upgrade deps. + - **FIX**: add compatibility for emoji font with flutter >= 3.22. + - **FIX**: Flutter 3.22 arg error. + +#### `studyu_flutter_common` - `v1.8.4-dev.1` + + - **FIX**: formatting issues. + + +## 2024-06-09 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`studyu_app` - `v2.7.6-dev.1`](#studyu_app---v276-dev1) + - [`studyu_designer_v2` - `v1.8.1-dev.1`](#studyu_designer_v2---v181-dev1) + +--- + +#### `studyu_app` - `v2.7.6-dev.1` + + - **FIX**(app): update android deployment. + - **FIX**(app): update ios deployment. + - **FIX**(app): update android deployment. + +#### `studyu_designer_v2` - `v1.8.1-dev.1` + + - **FIX**: add emojis again. + + +## 2024-06-07 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`studyu_app` - `v2.7.6-dev.0`](#studyu_app---v276-dev0) + - [`studyu_core` - `v4.4.3-dev.0`](#studyu_core---v443-dev0) + - [`studyu_designer_v2` - `v1.8.1-dev.0`](#studyu_designer_v2---v181-dev0) + - [`studyu_flutter_common` - `v1.8.4-dev.0`](#studyu_flutter_common---v184-dev0) + +--- + +#### `studyu_app` - `v2.7.6-dev.0` + + - **FIX**: upgrade deps. + - **FIX**: UI overflows. + +#### `studyu_core` - `v4.4.3-dev.0` + + - **FIX**: upgrade deps. + +#### `studyu_designer_v2` - `v1.8.1-dev.0` + + - **FIX**: integration test sign out. + - **FIX**: check if canPop. + - **FIX**: upgrade deps. + - **FIX**: add compatibility for emoji font with flutter >= 3.22. + - **FIX**: Flutter 3.22 arg error. + +#### `studyu_flutter_common` - `v1.8.4-dev.0` + + - **FIX**: upgrade deps. + - **FIX**: put supabase cli default anon key in env.local.example. + + ## 2024-05-13 ### Changes diff --git a/app/.metadata b/app/.metadata index eea2802f1..6eb54a17b 100644 --- a/app/.metadata +++ b/app/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "67457e669f79e9f8d13d7a68fe09775fefbb79f4" + revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49" channel: "stable" project_type: app @@ -13,26 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 - base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 - platform: android - create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 - base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 - platform: ios - create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 - base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 - platform: linux - create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 - base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 - platform: macos - create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 - base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 - platform: web - create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 - base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 - platform: windows - create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 - base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 # User provided section diff --git a/app/CHANGELOG.md b/app/CHANGELOG.md index cc511277d..7530e8eea 100644 --- a/app/CHANGELOG.md +++ b/app/CHANGELOG.md @@ -1,3 +1,26 @@ +## 2.7.6-dev.2 + + - **FIX**: formatting issues. + - **FIX**(app): update ios deployment. + - **FIX**(app): update android deployment. + - **FIX**: upgrade deps. + - **FIX**(app): update ios deployment. + - **FIX**(app): update android deployment. + - **FIX**(app): update android deployment. + - **FIX**(app): update ios deployment. + - **FIX**(app): update android deployment. + +## 2.7.6-dev.1 + + - **FIX**(app): update android deployment. + - **FIX**(app): update ios deployment. + - **FIX**(app): update android deployment. + +## 2.7.6-dev.0 + + - **FIX**: upgrade deps. + - **FIX**: UI overflows. + ## 2.7.5 - **FIX**: update podfile. diff --git a/app/analysis_options.yaml b/app/analysis_options.yaml index ca8edeaa3..f689b7b48 100644 --- a/app/analysis_options.yaml +++ b/app/analysis_options.yaml @@ -1 +1,7 @@ -include: ../flutter_analysis_options.yaml +include: ../analysis_options.yaml + +analyzer: + errors: + avoid_classes_with_only_static_members: ignore + plugins: + - custom_lint diff --git a/app/android/Gemfile.lock b/app/android/Gemfile.lock index cb09ef947..daec0568f 100644 --- a/app/android/Gemfile.lock +++ b/app/android/Gemfile.lock @@ -10,17 +10,17 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.914.0) - aws-sdk-core (3.192.0) + aws-partitions (1.943.0) + aws-sdk-core (3.197.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.79.0) - aws-sdk-core (~> 3, >= 3.191.0) + aws-sdk-kms (1.83.0) + aws-sdk-core (~> 3, >= 3.197.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.147.0) - aws-sdk-core (~> 3, >= 3.192.0) + aws-sdk-s3 (1.152.1) + aws-sdk-core (~> 3, >= 3.197.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) aws-sigv4 (1.8.0) @@ -147,7 +147,7 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.6) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) @@ -157,7 +157,7 @@ GEM mini_magick (4.12.0) mini_mime (1.1.5) multi_json (1.15.0) - multipart-post (2.4.0) + multipart-post (2.4.1) nanaimo (0.3.0) naturally (2.2.1) nkf (0.2.0) @@ -171,7 +171,8 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.6) + rexml (3.2.9) + strscan rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -184,6 +185,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + strscan (3.1.0) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index 0d97d49a1..dbbbe5092 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -1,25 +1,26 @@ plugins { id "com.android.application" id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id "dev.flutter.flutter-gradle-plugin" } def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') +def localPropertiesFile = rootProject.file("local.properties") if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> + localPropertiesFile.withReader("UTF-8") { reader -> localProperties.load(reader) } } -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +def flutterVersionCode = localProperties.getProperty("flutter.versionCode") if (flutterVersionCode == null) { - flutterVersionCode = '1' + flutterVersionCode = "1" } -def flutterVersionName = localProperties.getProperty('flutter.versionName') +def flutterVersionName = localProperties.getProperty("flutter.versionName") if (flutterVersionName == null) { - flutterVersionName = '1.0' + flutterVersionName = "1.0" } def keystoreProperties = new Properties() @@ -29,45 +30,39 @@ if (keystorePropertiesFile.exists()) { } android { - namespace "health.studyu.app" + namespace = "health.studyu.app" // Start flutter_local_notifications - compileSdkVersion Math.max(flutter.compileSdkVersion, 34) + compileSdkVersion = Math.max(flutter.compileSdkVersion, 34) // End flutter_local_notifications - - ndkVersion flutter.ndkVersion + // temp fix for record_audio package instead of "flutter.ndkVersion" + ndkVersion = "26.1.10909125" // Start flutter_local_notifications defaultConfig { - multiDexEnabled true + multiDexEnabled = true } // End flutter_local_notifications compileOptions { // Start flutter_local_notifications // Flag to enable support for the new language APIs - coreLibraryDesugaringEnabled true + coreLibraryDesugaringEnabled = true // End flutter_local_notifications - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + jvmTarget = '17' } defaultConfig { - applicationId "health.studyu.app" + applicationId = "health.studyu.app" // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration - // camera_android requires at least sdk 21 - minSdkVersion Math.max(flutter.minSdkVersion, 21) - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdk = Math.max(flutter.minSdkVersion, 23) + targetSdk = flutter.targetSdkVersion + versionCode = flutterVersionCode.toInteger() + versionName = flutterVersionName } signingConfigs { @@ -80,13 +75,13 @@ android { } buildTypes { release { - signingConfig keystorePropertiesFile.exists() ? signingConfigs.release : null + signingConfig = keystorePropertiesFile.exists() ? signingConfigs.release : null } } } flutter { - source '../..' + source = "../.." } dependencies { diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index 9a2bf234f..f195e55a9 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -34,6 +34,7 @@ android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" + android:taskAffinity="" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" @@ -68,4 +69,15 @@ android:name="flutterEmbedding" android:value="2" /> + + + + + + + diff --git a/app/android/app/src/main/kotlin/health/studyu/app/studyu_app/MainActivity.kt b/app/android/app/src/main/kotlin/health/studyu/app/MainActivity.kt similarity index 65% rename from app/android/app/src/main/kotlin/health/studyu/app/studyu_app/MainActivity.kt rename to app/android/app/src/main/kotlin/health/studyu/app/MainActivity.kt index fdeb24d36..2e44fe3cb 100644 --- a/app/android/app/src/main/kotlin/health/studyu/app/studyu_app/MainActivity.kt +++ b/app/android/app/src/main/kotlin/health/studyu/app/MainActivity.kt @@ -2,5 +2,4 @@ package health.studyu.app import io.flutter.embedding.android.FlutterActivity -class MainActivity: FlutterActivity() { -} +class MainActivity: FlutterActivity() diff --git a/app/android/build.gradle b/app/android/build.gradle index e83fb5dac..d2ffbffa4 100644 --- a/app/android/build.gradle +++ b/app/android/build.gradle @@ -1,15 +1,3 @@ -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() @@ -17,12 +5,12 @@ allprojects { } } -rootProject.buildDir = '../build' +rootProject.buildDir = "../build" subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { - project.evaluationDependsOn(':app') + project.evaluationDependsOn(":app") } tasks.register("clean", Delete) { diff --git a/app/android/gradle.properties b/app/android/gradle.properties index 598d13fee..3b5b324f6 100644 --- a/app/android/gradle.properties +++ b/app/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx4G +org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true diff --git a/app/android/gradle/wrapper/gradle-wrapper.properties b/app/android/gradle/wrapper/gradle-wrapper.properties index 3c472b99c..5af0f539c 100644 --- a/app/android/gradle/wrapper/gradle-wrapper.properties +++ b/app/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip diff --git a/app/android/settings.gradle b/app/android/settings.gradle index 7cd712855..2054f554f 100644 --- a/app/android/settings.gradle +++ b/app/android/settings.gradle @@ -5,25 +5,21 @@ pluginManagement { def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" return flutterSdkPath - } - settings.ext.flutterSdkPath = flutterSdkPath() + }() - includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") repositories { google() mavenCentral() gradlePluginPortal() } - - plugins { - id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false - } } plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.3.0" apply false + id "com.android.application" version "8.4.0" apply false + id "org.jetbrains.kotlin.android" version "1.9.23" apply false } include ":app" diff --git a/app/ios/Flutter/AppFrameworkInfo.plist b/app/ios/Flutter/AppFrameworkInfo.plist index 658d8c43c..7c5696400 100644 --- a/app/ios/Flutter/AppFrameworkInfo.plist +++ b/app/ios/Flutter/AppFrameworkInfo.plist @@ -2,25 +2,25 @@ - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 12.0 + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 diff --git a/app/ios/Gemfile.lock b/app/ios/Gemfile.lock index cb09ef947..daec0568f 100644 --- a/app/ios/Gemfile.lock +++ b/app/ios/Gemfile.lock @@ -10,17 +10,17 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.914.0) - aws-sdk-core (3.192.0) + aws-partitions (1.943.0) + aws-sdk-core (3.197.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.79.0) - aws-sdk-core (~> 3, >= 3.191.0) + aws-sdk-kms (1.83.0) + aws-sdk-core (~> 3, >= 3.197.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.147.0) - aws-sdk-core (~> 3, >= 3.192.0) + aws-sdk-s3 (1.152.1) + aws-sdk-core (~> 3, >= 3.197.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) aws-sigv4 (1.8.0) @@ -147,7 +147,7 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.6) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) @@ -157,7 +157,7 @@ GEM mini_magick (4.12.0) mini_mime (1.1.5) multi_json (1.15.0) - multipart-post (2.4.0) + multipart-post (2.4.1) nanaimo (0.3.0) naturally (2.2.1) nkf (0.2.0) @@ -171,7 +171,8 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.6) + rexml (3.2.9) + strscan rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -184,6 +185,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + strscan (3.1.0) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock index bf4adfc7d..209989e65 100644 --- a/app/ios/Podfile.lock +++ b/app/ios/Podfile.lock @@ -26,11 +26,11 @@ PODS: - record_darwin (1.0.0): - Flutter - FlutterMacOS - - Sentry/HybridSDK (8.25.0) - - sentry_flutter (8.1.0): + - Sentry/HybridSDK (8.25.2) + - sentry_flutter (8.2.0): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.25.0) + - Sentry/HybridSDK (= 8.25.2) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -126,15 +126,15 @@ SPEC CHECKSUMS: flutter_timezone: ffb07bdad3c6276af8dada0f11978d8a1f8a20bb just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 record_darwin: 1f6619f2abac4d1ca91d3eeab038c980d76f1517 - Sentry: cd86fc55628f5b7c572cabe66cc8f95a9d2f165a - sentry_flutter: ca7760fc008dc3bc2981730dc0c1d2f892178370 - shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + Sentry: 51b056d96914a741f63eca774d118678b1eb05a1 + sentry_flutter: e8397d13e297a5d4b6be8a752e33140b21c5cc97 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec - url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 - video_player_avfoundation: 2b4384f3b157206b5e150a0083cdc0c905d260d3 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36 diff --git a/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/app/ios/Runner/AppDelegate.swift b/app/ios/Runner/AppDelegate.swift index f0630aa0c..aa0522ca0 100644 --- a/app/ios/Runner/AppDelegate.swift +++ b/app/ios/Runner/AppDelegate.swift @@ -1,5 +1,5 @@ -import UIKit import Flutter +import UIKit import flutter_local_notifications @UIApplicationMain diff --git a/app/ios/Runner/Info.plist b/app/ios/Runner/Info.plist index 752966c2e..391233a29 100644 --- a/app/ios/Runner/Info.plist +++ b/app/ios/Runner/Info.plist @@ -2,8 +2,6 @@ - CADisableMinimumFrameDurationOnPhone - CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -71,9 +69,11 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - - NSCameraUsageDescription + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + NSCameraUsageDescription Need to access your camera to capture a photo for trial observations. diff --git a/app/ios/Runner/Runner.entitlements b/app/ios/Runner/Runner.entitlements index 28c29bf6d..951769cc4 100644 --- a/app/ios/Runner/Runner.entitlements +++ b/app/ios/Runner/Runner.entitlements @@ -1,4 +1,3 @@ - diff --git a/app/lib/app.dart b/app/lib/app.dart index 3c5d63835..293d87437 100644 --- a/app/lib/app.dart +++ b/app/lib/app.dart @@ -2,17 +2,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:studyu_app/main.dart'; +import 'package:studyu_app/models/app_state.dart'; +import 'package:studyu_app/routes.dart'; +import 'package:studyu_app/theme.dart'; import 'package:studyu_app/util/app_analytics.dart'; import 'package:studyu_core/core.dart'; import 'package:studyu_flutter_common/studyu_flutter_common.dart'; -import 'main.dart'; -import 'models/app_state.dart'; -import 'routes.dart'; -import 'theme.dart'; - class MyApp extends StatefulWidget { - const MyApp(this.queryParameters, this.appConfig, {super.key, required this.initialRoute}); + const MyApp( + this.queryParameters, + this.appConfig, { + super.key, + required this.initialRoute, + }); final Map queryParameters; final AppConfig? appConfig; final String initialRoute; @@ -31,7 +35,9 @@ class _MyAppState extends State { Widget build(BuildContext context) { return MultiProvider( providers: [ - ChangeNotifierProvider(create: (context) => AppLanguage(AppLocalizations.supportedLocales)), + ChangeNotifierProvider( + create: (context) => AppLanguage(AppLocalizations.supportedLocales), + ), ChangeNotifierProvider(create: (context) => AppState()), ], child: Consumer( @@ -50,7 +56,8 @@ class _MyAppState extends State { ], localeListResolutionCallback: (locales, supportedLocales) { // print('device locales=$locales supported locales=$supportedLocales'); - final supportedLanguageCodes = supportedLocales.map((e) => e.languageCode); + final supportedLanguageCodes = + supportedLocales.map((e) => e.languageCode); if (locales != null) { for (final locale in locales) { if (supportedLanguageCodes.contains(locale.languageCode)) { diff --git a/app/lib/constants.dart b/app/lib/constants.dart index 1d5f7292b..0b755bb22 100644 --- a/app/lib/constants.dart +++ b/app/lib/constants.dart @@ -1,2 +1,3 @@ -const String playStoreUrl = 'https://play.google.com/store/apps/details?id=health.studyu.app'; +const String playStoreUrl = + 'https://play.google.com/store/apps/details?id=health.studyu.app'; const String appstoreUrl = 'https://itunes.apple.com/app/id1571991198'; diff --git a/app/lib/main.dart b/app/lib/main.dart index a061af6a9..9de88a085 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -7,6 +7,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_timezone/flutter_timezone.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:studyu_app/app.dart'; import 'package:studyu_app/routes.dart'; import 'package:studyu_app/util/app_analytics.dart'; import 'package:studyu_core/core.dart'; @@ -14,8 +15,6 @@ import 'package:studyu_flutter_common/studyu_flutter_common.dart'; import 'package:timezone/data/latest_all.dart' as tz; import 'package:timezone/timezone.dart' as tz; -import 'app.dart'; - @pragma('vm:entry-point') void notificationTapBackground(NotificationResponse notificationResponse) { // ignore: avoid_print @@ -24,11 +23,14 @@ void notificationTapBackground(NotificationResponse notificationResponse) { ' payload: ${notificationResponse.payload}'); if (notificationResponse.input?.isNotEmpty ?? false) { // ignore: avoid_print - print('notification action tapped with input: ${notificationResponse.input}'); + print( + 'notification action tapped with input: ${notificationResponse.input}', + ); } } -GlobalKey navigatorKey = GlobalKey(debugLabel: 'Main Navigator'); +GlobalKey navigatorKey = + GlobalKey(debugLabel: 'Main Navigator'); Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -51,37 +53,47 @@ Future main() async { } await AppAnalytics.init(); - if (!kDebugMode && AppAnalytics.isUserEnabled) { - AppAnalytics.start(appConfig, MyApp(queryParameters, appConfig, initialRoute: initialRoute)); + if (!kDebugMode && + AppAnalytics.isUserEnabled != null && + AppAnalytics.isUserEnabled!) { + AppAnalytics.start( + appConfig, + MyApp(queryParameters, appConfig, initialRoute: initialRoute), + ); } else { runApp(MyApp(queryParameters, appConfig, initialRoute: initialRoute)); } - AppLifecycleListener(onResume: () async { - try { - final navigatorState = navigatorKey.currentState; - if (navigatorState == null) return; - String? currentRoute; - navigatorState.popUntil((route) { - currentRoute = route.settings.name; - return true; - }); - if (currentRoute == Routes.appOutdated) return; - final appConfig = await AppConfig.getAppConfig(); - if (await isAppOutdated(appConfig)) { - await navigatorState.pushNamedAndRemoveUntil(Routes.appOutdated, (route) => false); + AppLifecycleListener( + onResume: () async { + try { + final navigatorState = navigatorKey.currentState; + if (navigatorState == null) return; + String? currentRoute; + navigatorState.popUntil((route) { + currentRoute = route.settings.name; + return true; + }); + if (currentRoute == Routes.appOutdated) return; + final appConfig = await AppConfig.getAppConfig(); + if (await isAppOutdated(appConfig)) { + await navigatorState.pushNamedAndRemoveUntil( + Routes.appOutdated, + (route) => false, + ); + } + } catch (error) { + // device could be offline + debugPrint('Error fetching app config: $error'); } - } catch (error) { - // device could be offline - debugPrint('Error fetching app config: $error'); - } - }); + }, + ); } /// Checks major and minor version of the app against the minimum version required by the backend /// Returns true if the app is outdated Future isAppOutdated(AppConfig appConfig) async { - PackageInfo packageInfo = await PackageInfo.fromPlatform(); + final PackageInfo packageInfo = await PackageInfo.fromPlatform(); final appVersion = packageInfo.version; final appVersionParts = appVersion.split('.'); final minVersionParts = appConfig.appMinVersion.split('.'); @@ -92,7 +104,8 @@ Future isAppOutdated(AppConfig appConfig) async { final minVersionMajor = int.parse(minVersionParts[0]); final minVersionMinor = int.parse(minVersionParts[1]); - return appVersionMajor < minVersionMajor || (appVersionMajor == minVersionMajor && appVersionMinor < minVersionMinor); + return appVersionMajor < minVersionMajor || + (appVersionMajor == minVersionMajor && appVersionMinor < minVersionMinor); } /// This is needed for flutter_local_notifications diff --git a/app/lib/models/app_state.dart b/app/lib/models/app_state.dart index 6a4096129..0070628bb 100644 --- a/app/lib/models/app_state.dart +++ b/app/lib/models/app_state.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:studyu_app/util/app_analytics.dart'; -import 'package:studyu_app/util/notifications.dart'; import 'package:studyu_app/util/cache.dart'; +import 'package:studyu_app/util/notifications.dart'; import 'package:studyu_app/util/schedule_notifications.dart'; import 'package:studyu_core/core.dart'; diff --git a/app/lib/routes.dart b/app/lib/routes.dart index 6af899e7f..a0b9e0969 100644 --- a/app/lib/routes.dart +++ b/app/lib/routes.dart @@ -1,22 +1,21 @@ // ignore_for_file: avoid_classes_with_only_static_members import 'package:flutter/material.dart'; - -import 'screens/app_onboarding/about.dart'; -import 'screens/app_onboarding/loading_screen.dart'; -import 'screens/app_onboarding/terms.dart'; -import 'screens/app_onboarding/welcome.dart'; -import 'screens/app_onboarding/app_outdated_screen.dart'; -import 'screens/study/dashboard/contact_tab/contact_screen.dart'; -import 'screens/study/dashboard/contact_tab/faq.dart'; -import 'screens/study/dashboard/dashboard.dart'; -import 'screens/study/dashboard/settings.dart'; -import 'screens/study/onboarding/consent.dart'; -import 'screens/study/onboarding/intervention_selection.dart'; -import 'screens/study/onboarding/journey_overview.dart'; -import 'screens/study/onboarding/kickoff.dart'; -import 'screens/study/onboarding/study_overview.dart'; -import 'screens/study/onboarding/study_selection.dart'; -import 'screens/study/report/report_history.dart'; +import 'package:studyu_app/screens/app_onboarding/about.dart'; +import 'package:studyu_app/screens/app_onboarding/app_outdated_screen.dart'; +import 'package:studyu_app/screens/app_onboarding/loading_screen.dart'; +import 'package:studyu_app/screens/app_onboarding/terms.dart'; +import 'package:studyu_app/screens/app_onboarding/welcome.dart'; +import 'package:studyu_app/screens/study/dashboard/contact_tab/contact_screen.dart'; +import 'package:studyu_app/screens/study/dashboard/contact_tab/faq.dart'; +import 'package:studyu_app/screens/study/dashboard/dashboard.dart'; +import 'package:studyu_app/screens/study/dashboard/settings.dart'; +import 'package:studyu_app/screens/study/onboarding/consent.dart'; +import 'package:studyu_app/screens/study/onboarding/intervention_selection.dart'; +import 'package:studyu_app/screens/study/onboarding/journey_overview.dart'; +import 'package:studyu_app/screens/study/onboarding/kickoff.dart'; +import 'package:studyu_app/screens/study/onboarding/study_overview.dart'; +import 'package:studyu_app/screens/study/onboarding/study_selection.dart'; +import 'package:studyu_app/screens/study/report/report_history.dart'; class Routes { static const String loading = '/loading'; @@ -47,7 +46,9 @@ class Routes { child: Center( child: Padding( padding: const EdgeInsets.all(16), - child: Text('No route defined for ${settings.name}.\nThe developers should fix this 👩‍💻'), + child: Text( + 'No route defined for ${settings.name}.\nThe developers should fix this 👩‍💻', + ), ), ), ), @@ -55,43 +56,97 @@ class Routes { ); } - static Route? generateRoute(RouteSettings settings, Map queryParameters) { + static Route? generateRoute( + RouteSettings settings, + Map queryParameters, + ) { final uri = Uri.parse(settings.name!); switch (uri.path) { case loading: - return MaterialPageRoute(builder: (_) => const LoadingScreen(), settings: settings); + return MaterialPageRoute( + builder: (_) => const LoadingScreen(), + settings: settings, + ); case preview: - return MaterialPageRoute(builder: (_) => LoadingScreen(queryParameters: queryParameters), settings: settings); + return MaterialPageRoute( + builder: (_) => LoadingScreen(queryParameters: queryParameters), + settings: settings, + ); case appOutdated: - return MaterialPageRoute(builder: (_) => const AppOutdatedScreen(), settings: settings); + return MaterialPageRoute( + builder: (_) => const AppOutdatedScreen(), + settings: settings, + ); case dashboard: - return MaterialPageRoute(builder: (_) => const DashboardScreen(), settings: settings); + return MaterialPageRoute( + builder: (_) => const DashboardScreen(), + settings: settings, + ); case welcome: - return MaterialPageRoute(builder: (_) => const WelcomeScreen(), settings: settings); + return MaterialPageRoute( + builder: (_) => const WelcomeScreen(), + settings: settings, + ); case about: - return MaterialPageRoute(builder: (_) => const AboutScreen(), settings: settings); + return MaterialPageRoute( + builder: (_) => const AboutScreen(), + settings: settings, + ); case terms: - return MaterialPageRoute(builder: (_) => const TermsScreen(), settings: settings); + return MaterialPageRoute( + builder: (_) => const TermsScreen(), + settings: settings, + ); case studySelection: - return MaterialPageRoute(builder: (_) => const StudySelectionScreen(), settings: settings); + return MaterialPageRoute( + builder: (_) => const StudySelectionScreen(), + settings: settings, + ); case studyOverview: - return MaterialPageRoute(builder: (_) => const StudyOverviewScreen(), settings: settings); + return MaterialPageRoute( + builder: (_) => const StudyOverviewScreen(), + settings: settings, + ); case interventionSelection: - return MaterialPageRoute(builder: (_) => const InterventionSelectionScreen(), settings: settings); + return MaterialPageRoute( + builder: (_) => const InterventionSelectionScreen(), + settings: settings, + ); case journey: - return MaterialPageRoute(builder: (_) => const JourneyOverviewScreen(), settings: settings); + return MaterialPageRoute( + builder: (_) => const JourneyOverviewScreen(), + settings: settings, + ); case consent: - return MaterialPageRoute(builder: (_) => const ConsentScreen(), settings: settings); + return MaterialPageRoute( + builder: (_) => const ConsentScreen(), + settings: settings, + ); case kickoff: - return MaterialPageRoute(builder: (_) => const KickoffScreen(), settings: settings); + return MaterialPageRoute( + builder: (_) => const KickoffScreen(), + settings: settings, + ); case contact: - return MaterialPageRoute(builder: (_) => const ContactScreen(), settings: settings); + return MaterialPageRoute( + builder: (_) => const ContactScreen(), + settings: settings, + ); case faq: - return MaterialPageRoute(builder: (_) => const FAQ(), settings: settings); + return MaterialPageRoute( + builder: (_) => const FAQ(), + settings: settings, + ); case appSettings: - return MaterialPageRoute(builder: (_) => const Settings(), settings: settings); + return MaterialPageRoute( + builder: (_) => const Settings(), + settings: settings, + ); case reportHistory: - return MaterialPageRoute(builder: (_) => const ReportHistoryScreen(), settings: settings); + return MaterialPageRoute( + builder: (_) => const ReportHistoryScreen(), + settings: settings, + ); default: //final potentialSessionString = Uri.decodeComponent(settings.name.replaceFirst('/', '')); //return MaterialPageRoute(builder: (_) => LoadingScreen(sessionString: potentialSessionString)); diff --git a/app/lib/screens/app_onboarding/about.dart b/app/lib/screens/app_onboarding/about.dart index 1c7bae717..3f8606448 100644 --- a/app/lib/screens/app_onboarding/about.dart +++ b/app/lib/screens/app_onboarding/about.dart @@ -30,10 +30,15 @@ class AboutScreen extends StatelessWidget { child: Icon(MdiIcons.food, size: 80, color: Colors.black), ), Expanded( - child: Icon(MdiIcons.equal, size: 80, color: Colors.black), + child: + Icon(MdiIcons.equal, size: 80, color: Colors.black), ), Expanded( - child: Icon(MdiIcons.sleepOff, size: 80, color: Colors.black), + child: Icon( + MdiIcons.sleepOff, + size: 80, + color: Colors.black, + ), ), ], ), @@ -69,7 +74,8 @@ class AboutScreen extends StatelessWidget { Row( children: [ Expanded( - child: Icon(MdiIcons.help, size: 80, color: Colors.orange), + child: + Icon(MdiIcons.help, size: 80, color: Colors.orange), ), ], ), @@ -105,7 +111,11 @@ class AboutScreen extends StatelessWidget { Row( children: [ Expanded( - child: Icon(MdiIcons.accountQuestion, size: 80, color: Colors.blue), + child: Icon( + MdiIcons.accountQuestion, + size: 80, + color: Colors.blue, + ), ), ], ), @@ -141,8 +151,12 @@ class AboutScreen extends StatelessWidget { Row( children: [ Expanded( - child: Icon(MdiIcons.exclamationThick, size: 80, color: Colors.blue), - ) + child: Icon( + MdiIcons.exclamationThick, + size: 80, + color: Colors.blue, + ), + ), ], ), const SizedBox(height: 50), @@ -177,7 +191,11 @@ class AboutScreen extends StatelessWidget { Row( children: [ Expanded( - child: Icon(MdiIcons.alphaNBoxOutline, size: 80, color: Colors.blue), + child: Icon( + MdiIcons.alphaNBoxOutline, + size: 80, + color: Colors.blue, + ), ), const Expanded( child: Text( @@ -187,7 +205,11 @@ class AboutScreen extends StatelessWidget { ), ), Expanded( - child: Icon(MdiIcons.numeric1BoxOutline, size: 80, color: Colors.blue), + child: Icon( + MdiIcons.numeric1BoxOutline, + size: 80, + color: Colors.blue, + ), ), ], ), @@ -223,7 +245,11 @@ class AboutScreen extends StatelessWidget { Row( children: [ Expanded( - child: Icon(MdiIcons.notebookOutline, size: 80, color: Colors.blue), + child: Icon( + MdiIcons.notebookOutline, + size: 80, + color: Colors.blue, + ), ), ], ), @@ -259,7 +285,11 @@ class AboutScreen extends StatelessWidget { Row( children: [ Expanded( - child: Icon(MdiIcons.alignVerticalBottom, size: 80, color: Colors.blue), + child: Icon( + MdiIcons.alignVerticalBottom, + size: 80, + color: Colors.blue, + ), ), ], ), @@ -295,7 +325,11 @@ class AboutScreen extends StatelessWidget { Row( children: [ Expanded( - child: Icon(MdiIcons.progressCheck, size: 80, color: Colors.blue), + child: Icon( + MdiIcons.progressCheck, + size: 80, + color: Colors.blue, + ), ), ], ), @@ -327,7 +361,10 @@ class AboutScreen extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - const Image(image: AssetImage('assets/icon/logo.png'), height: 200), + const Image( + image: AssetImage('assets/icon/logo.png'), + height: 200, + ), const SizedBox(height: 50), Text( AppLocalizations.of(context)!.description_part9, @@ -339,7 +376,10 @@ class AboutScreen extends StatelessWidget { OutlinedButton.icon( icon: Icon(MdiIcons.rocket), onPressed: () => Navigator.pushNamed(context, Routes.terms), - label: Text(AppLocalizations.of(context)!.get_started, style: const TextStyle(fontSize: 20)), + label: Text( + AppLocalizations.of(context)!.get_started, + style: const TextStyle(fontSize: 20), + ), ), ], ), diff --git a/app/lib/screens/app_onboarding/app_outdated_screen.dart b/app/lib/screens/app_onboarding/app_outdated_screen.dart index 61e18288c..876ecf49e 100644 --- a/app/lib/screens/app_onboarding/app_outdated_screen.dart +++ b/app/lib/screens/app_onboarding/app_outdated_screen.dart @@ -29,23 +29,36 @@ class AppOutdatedScreen extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ const Spacer(), - const Image(image: AssetImage('assets/icon/logo.png'), height: 200), + const Image( + image: AssetImage('assets/icon/logo.png'), + height: 200, + ), const SizedBox(height: 20), Padding( padding: const EdgeInsets.all(20), - child: - Text(loc.app_outdated_message, textAlign: TextAlign.center, style: const TextStyle(fontSize: 20)), + child: Text( + loc.app_outdated_message, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 20), + ), ), const Spacer(), - storeUrl != null && storeIcon != null - ? OutlinedButton.icon( - icon: Icon(storeIcon), - onPressed: () async { - await launchUrl(Uri.parse(storeUrl!), mode: LaunchMode.externalNonBrowserApplication); - }, - label: Text(loc.update_now, style: const TextStyle(fontSize: 20)), - ) - : const SizedBox.shrink(), + if (storeUrl != null && storeIcon != null) + OutlinedButton.icon( + icon: Icon(storeIcon), + onPressed: () async { + await launchUrl( + Uri.parse(storeUrl!), + mode: LaunchMode.externalNonBrowserApplication, + ); + }, + label: Text( + loc.update_now, + style: const TextStyle(fontSize: 20), + ), + ) + else + const SizedBox.shrink(), const Spacer(), ], ), diff --git a/app/lib/screens/app_onboarding/loading_screen.dart b/app/lib/screens/app_onboarding/loading_screen.dart index a719cfefb..2b218d67f 100644 --- a/app/lib/screens/app_onboarding/loading_screen.dart +++ b/app/lib/screens/app_onboarding/loading_screen.dart @@ -3,15 +3,15 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +import 'package:studyu_app/models/app_state.dart'; +import 'package:studyu_app/routes.dart'; import 'package:studyu_app/screens/app_onboarding/iframe_helper.dart'; +import 'package:studyu_app/screens/app_onboarding/preview.dart'; import 'package:studyu_app/screens/study/onboarding/eligibility_screen.dart'; import 'package:studyu_app/screens/study/tasks/task_screen.dart'; import 'package:studyu_app/util/cache.dart'; import 'package:studyu_core/core.dart'; import 'package:studyu_flutter_common/studyu_flutter_common.dart'; -import 'package:studyu_app/models/app_state.dart'; -import 'package:studyu_app/routes.dart'; -import 'preview.dart'; class LoadingScreen extends StatefulWidget { final String? sessionString; @@ -71,7 +71,9 @@ class _LoadingScreenState extends State { try { subject = await _fetchRemoteSubject(selectedStudyObjectId); } catch (exception) { - StudyULogger.warning("Could not retrieve subject, maybe JWT is expired, try logging in: ${exception.toString()}"); + StudyULogger.warning( + "Could not retrieve subject, maybe JWT is expired, try logging in: $exception", + ); try { // Try signing in again. Needed if JWT is expired if (await signInParticipant()) { @@ -90,7 +92,7 @@ class _LoadingScreenState extends State { // 4. Open the app but do not join a study // 5. Restart the app. Either only this error shows up, worst case is // app hangs and is unresponsive - StudyULogger.fatal('Could not login and retrieve the study subject.' + StudyULogger.fatal('Could not login and retrieve the study subject. ' 'One reason for this might be that the study subject is no ' 'longer available and only resides in app backup'); throw Exception("Remote subject not found"); @@ -100,12 +102,16 @@ class _LoadingScreenState extends State { return subject; } - _initPreview(AppState state) async { + Future _initPreview(AppState state) async { if (state.isPreview) previewSubjectIdKey(); - if (widget.queryParameters == null || widget.queryParameters!.isEmpty) return; + if (widget.queryParameters == null || widget.queryParameters!.isEmpty) { + return; + } - StudyULogger.info("Preview: Found query parameters ${widget.queryParameters}"); - var lang = context.watch(); + StudyULogger.info( + "Preview: Found query parameters ${widget.queryParameters}", + ); + final lang = context.watch(); final preview = Preview( widget.queryParameters, lang, @@ -131,7 +137,10 @@ class _LoadingScreenState extends State { if (preview.selectedRoute == '/eligibilityCheck') { if (!mounted) return; // if we remove the await, we can push multiple times. warning: do not run in while(true) - await Navigator.push(context, EligibilityScreen.routeFor(study: preview.study)); + await Navigator.push( + context, + EligibilityScreen.routeFor(study: preview.study), + ); // either do the same navigator push again or --> send a message back to designer and let it reload the whole page <-- iFrameHelper.postRouteFinished(); return; @@ -145,7 +154,8 @@ class _LoadingScreenState extends State { return; } - state.activeSubject = await preview.getStudySubject(state, createSubject: true); + state.activeSubject = + await preview.getStudySubject(state, createSubject: true); // CONSENT if (preview.selectedRoute == Routes.consent) { @@ -188,13 +198,19 @@ class _LoadingScreenState extends State { if (preview.selectedRoute == '/observation') { print(state.selectedStudy!.observations.first.id); final tasks = [ - ...state.selectedStudy!.observations.where((observation) => observation.id == preview.extra), + ...state.selectedStudy!.observations + .where((observation) => observation.id == preview.extra), ]; if (!mounted) return; await Navigator.push( - context, - TaskScreen.routeFor( - taskInstance: TaskInstance(tasks.first, tasks.first.schedule.completionPeriods.first.id))); + context, + TaskScreen.routeFor( + taskInstance: TaskInstance( + tasks.first, + tasks.first.schedule.completionPeriods.first.id, + ), + ), + ); iFrameHelper.postRouteFinished(); return; } diff --git a/app/lib/screens/app_onboarding/preview.dart b/app/lib/screens/app_onboarding/preview.dart index 8ce71c21a..709c25fd6 100644 --- a/app/lib/screens/app_onboarding/preview.dart +++ b/app/lib/screens/app_onboarding/preview.dart @@ -50,7 +50,8 @@ class Preview { } if (containsQuery('data')) { - final data = jsonDecode(queryParameters!['data']!) as Map; + final data = + jsonDecode(queryParameters!['data']!) as Map; study = Study.fromJson(data); } else { study = await SupabaseQuery.getById(queryParameters!['studyid']!); @@ -108,7 +109,8 @@ class Preview { } bool containsQuery(String key) { - return queryParameters!.containsKey(key) && queryParameters![key]!.isNotEmpty; + return queryParameters!.containsKey(key) && + queryParameters![key]!.isNotEmpty; } bool containsQueryPair(String key, String value) { @@ -116,7 +118,10 @@ class Preview { } /// createSubject: If true, the method will return a new StudySubject if none can be found. Otherwise, null is returned - Future getStudySubject(AppState state, {bool createSubject = false}) async { + Future getStudySubject( + AppState state, { + bool createSubject = false, + }) async { if (selectedStudyObjectId != null) { try { if (selectedRoute == '/intervention') { @@ -134,10 +139,13 @@ class Preview { (foundSubject) { // todo baseline foundSubject.study.schedule.includeBaseline = false; - return foundSubject.userId == Supabase.instance.client.auth.currentUser!.id && + return foundSubject.userId == + Supabase.instance.client.auth.currentUser!.id && foundSubject.studyId == study!.id && listEquals( - foundSubject.selectedInterventions.map((i) => i.id).toList(), + foundSubject.selectedInterventions + .map((i) => i.id) + .toList(), getInterventionIds(), ); }, diff --git a/app/lib/screens/app_onboarding/terms.dart b/app/lib/screens/app_onboarding/terms.dart index 11f299ffd..1cc933944 100644 --- a/app/lib/screens/app_onboarding/terms.dart +++ b/app/lib/screens/app_onboarding/terms.dart @@ -2,13 +2,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:studyu_app/routes.dart'; +import 'package:studyu_app/widgets/bottom_onboarding_navigation.dart'; import 'package:studyu_core/core.dart'; import 'package:studyu_flutter_common/studyu_flutter_common.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../routes.dart'; -import '../../widgets/bottom_onboarding_navigation.dart'; - class TermsScreen extends StatefulWidget { const TermsScreen({super.key}); @@ -30,8 +29,10 @@ class _TermsScreenState extends State { body: SafeArea( child: Center( child: RetryFutureBuilder( - tryFunction: AppConfig.getAppConfig, - successBuilder: (BuildContext context, AppConfig? appConfig) => legalSection(context, appConfig)), + tryFunction: AppConfig.getAppConfig, + successBuilder: (BuildContext context, AppConfig? appConfig) => + legalSection(context, appConfig), + ), ), ), bottomNavigationBar: BottomOnboardingNavigation( @@ -63,7 +64,7 @@ class _TermsScreenState extends State { onChange: (val) => setState(() => _acceptedTerms = val!), isChecked: _acceptedTerms, icon: Icon(MdiIcons.fileDocumentEdit), - pdfUrl: appConfig!.appTerms[appLocale.languageCode.toString()], + pdfUrl: appConfig!.appTerms[appLocale.languageCode], pdfUrlLabel: AppLocalizations.of(context)!.terms_read, ), const SizedBox(height: 20), @@ -74,14 +75,16 @@ class _TermsScreenState extends State { onChange: (val) => setState(() => _acceptedPrivacy = val!), isChecked: _acceptedPrivacy, icon: Icon(MdiIcons.shieldLock), - pdfUrl: appConfig.appPrivacy[appLocale.languageCode.toString()], + pdfUrl: appConfig.appPrivacy[appLocale.languageCode], pdfUrlLabel: AppLocalizations.of(context)!.privacy_read, ), const SizedBox(height: 30), OutlinedButton.icon( icon: Icon(MdiIcons.scaleBalance), onPressed: () async { - final uri = Uri.parse(appConfig.imprint[appLocale.languageCode.toString()]!); + final uri = Uri.parse( + appConfig.imprint[appLocale.languageCode]!, + ); if (await canLaunchUrl(uri)) { launchUrl(uri, mode: LaunchMode.externalApplication); } @@ -122,12 +125,16 @@ class LegalSection extends StatelessWidget { final theme = Theme.of(context); return Column( children: [ - Text(title!, style: theme.textTheme.headlineMedium!.copyWith(color: theme.primaryColor)), + Text( + title!, + style: theme.textTheme.headlineMedium! + .copyWith(color: theme.primaryColor), + ), const SizedBox(height: 20), Text(description!), const SizedBox(height: 20), OutlinedButton.icon( - icon: icon!, + icon: icon, onPressed: () async { final uri = Uri.parse(pdfUrl!); if (await canLaunchUrl(uri)) { @@ -136,7 +143,11 @@ class LegalSection extends StatelessWidget { }, label: Text(pdfUrlLabel!), ), - CheckboxListTile(title: Text(acknowledgment!), value: isChecked, onChanged: onChange), + CheckboxListTile( + title: Text(acknowledgment!), + value: isChecked, + onChanged: onChange, + ), ], ); } diff --git a/app/lib/screens/app_onboarding/welcome.dart b/app/lib/screens/app_onboarding/welcome.dart index 48b17429c..175f5c04a 100644 --- a/app/lib/screens/app_onboarding/welcome.dart +++ b/app/lib/screens/app_onboarding/welcome.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; -import '../../routes.dart'; +import 'package:studyu_app/routes.dart'; class WelcomeScreen extends StatelessWidget { const WelcomeScreen({super.key}); @@ -16,32 +16,47 @@ class WelcomeScreen extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ const Spacer(), - const Image(image: AssetImage('assets/icon/logo.png'), height: 200), + const Image( + image: AssetImage('assets/icon/logo.png'), + height: 200, + ), const SizedBox(height: 20), OutlinedButton.icon( icon: const Icon(Icons.info), onPressed: () => Navigator.pushNamed(context, Routes.about), - label: Text(AppLocalizations.of(context)!.what_is_studyu, style: const TextStyle(fontSize: 20)), + label: Text( + AppLocalizations.of(context)!.what_is_studyu, + style: const TextStyle(fontSize: 20), + ), ), const SizedBox(height: 20), OutlinedButton.icon( icon: Icon(MdiIcons.accountBox), onPressed: () => Navigator.pushNamed(context, Routes.contact), - label: Text(AppLocalizations.of(context)!.contact, style: const TextStyle(fontSize: 20)), + label: Text( + AppLocalizations.of(context)!.contact, + style: const TextStyle(fontSize: 20), + ), ), const SizedBox(height: 20), OutlinedButton.icon( icon: Icon(MdiIcons.frequentlyAskedQuestions), onPressed: () => Navigator.pushNamed(context, Routes.faq), - label: Text(AppLocalizations.of(context)!.faq, style: const TextStyle(fontSize: 20)), + label: Text( + AppLocalizations.of(context)!.faq, + style: const TextStyle(fontSize: 20), + ), ), const Spacer(), OutlinedButton.icon( icon: Icon(MdiIcons.rocket, size: 30), onPressed: () => Navigator.pushNamed(context, Routes.terms), - label: Text(AppLocalizations.of(context)!.get_started, style: const TextStyle(fontSize: 20)), + label: Text( + AppLocalizations.of(context)!.get_started, + style: const TextStyle(fontSize: 20), + ), ), - const Spacer() + const Spacer(), ], ), ), diff --git a/app/lib/screens/study/dashboard/contact_tab/contact_screen.dart b/app/lib/screens/study/dashboard/contact_tab/contact_screen.dart index 0302741ba..a2d9da904 100644 --- a/app/lib/screens/study/dashboard/contact_tab/contact_screen.dart +++ b/app/lib/screens/study/dashboard/contact_tab/contact_screen.dart @@ -2,12 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:provider/provider.dart'; +import 'package:studyu_app/models/app_state.dart'; import 'package:studyu_core/core.dart'; import 'package:studyu_flutter_common/studyu_flutter_common.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../../models/app_state.dart'; - class ContactScreen extends StatefulWidget { const ContactScreen({super.key}); @@ -42,7 +41,9 @@ class _ContactScreenState extends State { ), RetryFutureBuilder( tryFunction: AppConfig.getAppContact, - successBuilder: (BuildContext context, Contact? appSupportContact) => ContactWidget( + successBuilder: + (BuildContext context, Contact? appSupportContact) => + ContactWidget( contact: appSupportContact, title: AppLocalizations.of(context)!.app_support, subtitle: AppLocalizations.of(context)!.app_support_text, @@ -68,7 +69,13 @@ class ContactWidget extends StatelessWidget { final String? subtitle; final Color color; - const ContactWidget({required this.contact, required this.title, required this.color, this.subtitle, super.key}); + const ContactWidget({ + required this.contact, + required this.title, + required this.color, + this.subtitle, + super.key, + }); @override Widget build(BuildContext context) { @@ -77,9 +84,16 @@ class ContactWidget extends StatelessWidget { return Container(); } - final titles = [Text(title, style: theme.textTheme.titleLarge!.copyWith(color: color))]; + final titles = [ + Text(title, style: theme.textTheme.titleLarge!.copyWith(color: color)), + ]; if (subtitle != null && subtitle!.isNotEmpty) { - titles.add(Text(subtitle!, style: theme.textTheme.titleMedium!.copyWith(fontSize: 14))); + titles.add( + Text( + subtitle!, + style: theme.textTheme.titleMedium!.copyWith(fontSize: 14), + ), + ); } return Column( @@ -98,7 +112,9 @@ class ContactWidget extends StatelessWidget { ContactItem( itemName: AppLocalizations.of(context)!.irb, itemValue: contact!.institutionalReviewBoard! + - (contact?.institutionalReviewBoardNumber != null ? ': ${contact?.institutionalReviewBoardNumber}' : ''), + (contact?.institutionalReviewBoardNumber != null + ? ': ${contact?.institutionalReviewBoardNumber}' + : ''), iconData: MdiIcons.clipboardCheck, iconColor: color, ), @@ -164,18 +180,16 @@ class ContactItem extends StatelessWidget { Uri uri; switch (type) { case ContactItemType.website: - if (!itemValue!.startsWith('http://') && !itemValue!.startsWith('https://')) { + if (!itemValue!.startsWith('http://') && + !itemValue!.startsWith('https://')) { uri = Uri.parse('http://$itemValue'); } else { uri = Uri.parse(itemValue!); } - break; case ContactItemType.email: uri = Uri.parse('mailto:$itemValue'); - break; case ContactItemType.phone: uri = Uri.parse('tel:$itemValue'); - break; default: uri = Uri.parse(itemValue!); } @@ -195,7 +209,11 @@ class ContactItem extends StatelessWidget { return ListTile( title: Text(itemName), subtitle: SelectableText(itemValue!), - leading: Icon(iconData, color: iconColor ?? Theme.of(context).primaryColor, size: iconSize), + leading: Icon( + iconData, + color: iconColor ?? Theme.of(context).primaryColor, + size: iconSize, + ), onTap: type != null ? launchContact : null, ); } diff --git a/app/lib/screens/study/dashboard/contact_tab/faq.dart b/app/lib/screens/study/dashboard/contact_tab/faq.dart index 997cc04f3..97e1cfc5a 100644 --- a/app/lib/screens/study/dashboard/contact_tab/faq.dart +++ b/app/lib/screens/study/dashboard/contact_tab/faq.dart @@ -7,7 +7,8 @@ class FAQ extends StatelessWidget { @override Widget build(BuildContext context) { // TODO(Manisha): Transfer strings to translation files - if (AppLocalizations.of(context)!.faq_full == 'Frequently Asked Questions') { + if (AppLocalizations.of(context)!.faq_full == + 'Frequently Asked Questions') { return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.faq_full), @@ -68,7 +69,9 @@ final List data_en = [ Entry( 'How long will the study take to finish?', [ - Entry('The duration of each study is mentioned during initial study selection.'), + Entry( + 'The duration of each study is mentioned during initial study selection.', + ), ], ), Entry( @@ -93,7 +96,9 @@ final List data_en = [ Entry( 'How can I Opt out from the current study?', [ - Entry('You can do so by going to the Settings tab located on the Dashboard and clicking on "Opt-out" '), + Entry( + 'You can do so by going to the Settings tab located on the Dashboard and clicking on "Opt-out" ', + ), ], ), ], @@ -120,7 +125,9 @@ final List data_en = [ Entry( 'How can I keep track of my activities?', [ - Entry('You can get an overview of your daily tasks and health status in the "Reports History section"'), + Entry( + 'You can get an overview of your daily tasks and health status in the "Reports History section"', + ), ], ), Entry( @@ -159,7 +166,9 @@ final data_de = [ Entry( 'Welche persönlichen Daten sammelt die App?', [ - Entry('Die App sammelt keine persönlichen Daten des Benutzers, muss jedoch auf Zeit und Ort zugreifen.'), + Entry( + 'Die App sammelt keine persönlichen Daten des Benutzers, muss jedoch auf Zeit und Ort zugreifen.', + ), ], ), ], @@ -170,7 +179,9 @@ final data_de = [ Entry( 'Wie lange dauert es, bis die Studie abgeschlossen ist?', [ - Entry('Die Dauer jeder Studie wird bei der ersten Studienauswahl angegeben.'), + Entry( + 'Die Dauer jeder Studie wird bei der ersten Studienauswahl angegeben.', + ), ], ), Entry( diff --git a/app/lib/screens/study/dashboard/dashboard.dart b/app/lib/screens/study/dashboard/dashboard.dart index 90041fc96..108970a69 100644 --- a/app/lib/screens/study/dashboard/dashboard.dart +++ b/app/lib/screens/study/dashboard/dashboard.dart @@ -9,16 +9,15 @@ import 'package:material_design_icons_flutter/material_design_icons_flutter.dart import 'package:package_info_plus/package_info_plus.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; +import 'package:studyu_app/models/app_state.dart'; +import 'package:studyu_app/routes.dart'; +import 'package:studyu_app/screens/study/dashboard/task_overview_tab/task_overview.dart'; +import 'package:studyu_app/screens/study/report/report_details.dart'; import 'package:studyu_app/util/notifications.dart'; import 'package:studyu_app/util/schedule_notifications.dart'; import 'package:studyu_core/core.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../models/app_state.dart'; -import '../../../routes.dart'; -import '../report/report_details.dart'; -import 'task_overview_tab/task_overview.dart'; - class DashboardScreen extends StatefulWidget { final String? error; @@ -37,11 +36,14 @@ class OverflowMenuItem { OverflowMenuItem(this.name, this.icon, {this.routeName, this.onTap}); } -class _DashboardScreenState extends State with WidgetsBindingObserver { +class _DashboardScreenState extends State + with WidgetsBindingObserver { StudySubject? subject; List? scheduleToday; - get showNextDay => (kDebugMode || context.read().isPreview) && !subject!.completedStudy; + bool get showNextDay => + (kDebugMode || context.read().isPreview) && + !subject!.completedStudy; @override void initState() { @@ -56,7 +58,6 @@ class _DashboardScreenState extends State with WidgetsBindingOb setState(() { scheduleToday = subject!.scheduleFor(DateTime.now()); }); - break; case AppLifecycleState.inactive: break; case AppLifecycleState.paused: @@ -76,7 +77,8 @@ class _DashboardScreenState extends State with WidgetsBindingOb scheduleToday = subject!.scheduleFor(DateTime.now()); if (widget.error != null) { WidgetsBinding.instance.addPostFrameCallback((_) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(widget.error!))); + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(widget.error!))); }); } } @@ -96,7 +98,11 @@ class _DashboardScreenState extends State with WidgetsBindingOb Widget build(BuildContext context) { if (subject == null) { SchedulerBinding.instance.addPostFrameCallback((_) { - Navigator.pushNamedAndRemoveUntil(context, Routes.loading, (_) => false); + Navigator.pushNamedAndRemoveUntil( + context, + Routes.loading, + (_) => false, + ); }); return const SizedBox.shrink(); } @@ -117,14 +123,17 @@ class _DashboardScreenState extends State with WidgetsBindingOb IconButton( tooltip: 'Current report', // todo tr icon: Icon(MdiIcons.chartBar), - onPressed: () => Navigator.push(context, ReportDetailsScreen.routeFor(subject: subject!)), + onPressed: () => Navigator.push( + context, + ReportDetailsScreen.routeFor(subject: subject!), + ), ), PopupMenuButton( onSelected: (value) { if (value.routeName != null) { Navigator.pushNamed(context, value.routeName!); - } else if (value.onTap != null) { - value.onTap!(); + } else { + value.onTap?.call(); } }, itemBuilder: (context) { @@ -139,7 +148,11 @@ class _DashboardScreenState extends State with WidgetsBindingOb MdiIcons.frequentlyAskedQuestions, routeName: Routes.faq, ), - OverflowMenuItem(AppLocalizations.of(context)!.settings, Icons.settings, routeName: Routes.appSettings), + OverflowMenuItem( + AppLocalizations.of(context)!.settings, + Icons.settings, + routeName: Routes.appSettings, + ), OverflowMenuItem( AppLocalizations.of(context)!.what_is_studyu, MdiIcons.helpCircleOutline, @@ -150,53 +163,73 @@ class _DashboardScreenState extends State with WidgetsBindingOb MdiIcons.informationOutline, onTap: () async { final iconAuthors = ['Kiranshastry']; - final PackageInfo packageInfo = await PackageInfo.fromPlatform(); + final PackageInfo packageInfo = + await PackageInfo.fromPlatform(); if (!context.mounted) return; showAboutDialog( context: context, applicationIcon: GestureDetector( onDoubleTap: () { showDialog( - context: context, - builder: (_) => AlertDialog( - title: const SelectableText('Notification Log'), - content: Column( - children: [ - ElevatedButton( - onPressed: () { - final Uri emailLaunchUri = Uri( - scheme: 'mailto', - path: subject!.study.contact.email, - queryParameters: { - 'subject': '[StudyU] Debug Information', - 'body': StudyNotifications.scheduledNotificationsDebug, - }); - launchUrl(emailLaunchUri); - }, - child: const Text('Send via email'), - ), - FutureBuilder( - future: receivePermission(), - builder: (context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - String data = "ignoreBatteryOptimizations: ${snapshot.data.toString()}"; - StudyNotifications.scheduledNotificationsDebug = - "${StudyNotifications.scheduledNotificationsDebug}\n\n$data\n"; - return Text(data); - } else { - return const CircularProgressIndicator(); - } - }), - SelectableText(StudyNotifications.scheduledNotificationsDebug!), - ], - ), - scrollable: true, - )); + context: context, + builder: (_) => AlertDialog( + title: const SelectableText( + 'Notification Log', + ), + content: Column( + children: [ + ElevatedButton( + onPressed: () { + final Uri emailLaunchUri = Uri( + scheme: 'mailto', + path: subject!.study.contact.email, + queryParameters: { + 'subject': + '[StudyU] Debug Information', + 'body': StudyNotifications + .scheduledNotificationsDebug, + }, + ); + launchUrl(emailLaunchUri); + }, + child: const Text('Send via email'), + ), + FutureBuilder( + future: receivePermission(), + builder: ( + context, + AsyncSnapshot snapshot, + ) { + if (snapshot.hasData) { + final String data = + "ignoreBatteryOptimizations: ${snapshot.data}"; + StudyNotifications + .scheduledNotificationsDebug = + "${StudyNotifications.scheduledNotificationsDebug}\n\n$data\n"; + return Text(data); + } else { + return const CircularProgressIndicator(); + } + }, + ), + SelectableText( + StudyNotifications + .scheduledNotificationsDebug!, + ), + ], + ), + scrollable: true, + ), + ); testNotifications(context); }, - child: const Image(image: AssetImage('assets/icon/icon.png'), height: 32), + child: const Image( + image: AssetImage('assets/icon/icon.png'), + height: 32, + ), ), - applicationVersion: '${packageInfo.version} - ${packageInfo.buildNumber}', + applicationVersion: + '${packageInfo.version} - ${packageInfo.buildNumber}', children: [ RichText( text: TextSpan( @@ -208,7 +241,9 @@ class _DashboardScreenState extends State with WidgetsBindingOb text: 'www.flaticon.com', recognizer: TapGestureRecognizer() ..onTap = () { - launchUrl(Uri.parse('https://www.flaticon.com/')); + launchUrl( + Uri.parse('https://www.flaticon.com/'), + ); }, ), const TextSpan(text: ' made by'), @@ -234,16 +269,20 @@ class _DashboardScreenState extends State with WidgetsBindingOb ), ) .toList(), - ) + ), ], ); }, - ) + ), ].map((choice) { return PopupMenuItem( value: choice, child: Row( - children: [Icon(choice.icon, color: Colors.black), const SizedBox(width: 8), Text(choice.name)], + children: [ + Icon(choice.icon, color: Colors.black), + const SizedBox(width: 8), + Text(choice.name), + ], ), ); }).toList(); @@ -252,7 +291,9 @@ class _DashboardScreenState extends State with WidgetsBindingOb ], ), body: Padding( - padding: showNextDay ? EdgeInsets.only(bottom: MediaQuery.of(context).size.height / 10) : EdgeInsets.zero, + padding: showNextDay + ? EdgeInsets.only(bottom: MediaQuery.of(context).size.height / 10) + : EdgeInsets.zero, child: _buildBody(), ), bottomSheet: showNextDay @@ -284,14 +325,23 @@ class _DashboardScreenState extends State with WidgetsBindingOb } else if (subject!.startedAt!.isAfter(DateTime.now())) { final theme = Theme.of(context); return Center( - child: Padding( - padding: const EdgeInsets.fromLTRB(32, 32, 32, 32), - child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - AppLocalizations.of(context)!.study_not_started, - style: TextStyle(fontSize: 20, color: theme.primaryColor, fontWeight: FontWeight.bold), - ) - ]))); + child: Padding( + padding: const EdgeInsets.fromLTRB(32, 32, 32, 32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + AppLocalizations.of(context)!.study_not_started, + style: TextStyle( + fontSize: 20, + color: theme.primaryColor, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ); } else { return TaskOverview( subject: subject, @@ -320,19 +370,31 @@ class StudyFinishedPlaceholder extends StatelessWidget { children: [ Text( AppLocalizations.of(context)!.completed_study, - style: TextStyle(fontSize: 20, color: theme.primaryColor, fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: 20, + color: theme.primaryColor, + fontWeight: FontWeight.bold, + ), ), space, OutlinedButton.icon( - onPressed: () => Navigator.pushNamed(context, Routes.reportHistory), + onPressed: () => + Navigator.pushNamed(context, Routes.reportHistory), icon: Icon(MdiIcons.history, size: fontSize), - label: Text(AppLocalizations.of(context)!.report_history, style: textStyle), + label: Text( + AppLocalizations.of(context)!.report_history, + style: textStyle, + ), ), space, OutlinedButton.icon( - onPressed: () => Navigator.pushNamed(context, Routes.studySelection), + onPressed: () => + Navigator.pushNamed(context, Routes.studySelection), icon: Icon(MdiIcons.clipboardArrowRightOutline, size: fontSize), - label: Text(AppLocalizations.of(context)!.study_selection, style: textStyle), + label: Text( + AppLocalizations.of(context)!.study_selection, + style: textStyle, + ), ), ], ), diff --git a/app/lib/screens/study/dashboard/settings.dart b/app/lib/screens/study/dashboard/settings.dart index 6e0eaafee..485666fb9 100644 --- a/app/lib/screens/study/dashboard/settings.dart +++ b/app/lib/screens/study/dashboard/settings.dart @@ -70,29 +70,33 @@ class _SettingsState extends State { ), ], ), - Row(mainAxisSize: MainAxisSize.min, children: [ - Text('${AppLocalizations.of(context)!.allow_analytics}: '), - Tooltip( - triggerMode: TooltipTriggerMode.tap, - showDuration: const Duration(milliseconds: 10000), - margin: const EdgeInsets.fromLTRB(30, 0, 30, 0), - message: AppLocalizations.of(context)!.allow_analytics_desc, - child: const Icon( - Icons.info, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('${AppLocalizations.of(context)!.allow_analytics}: '), + Tooltip( + triggerMode: TooltipTriggerMode.tap, + showDuration: const Duration(milliseconds: 10000), + margin: const EdgeInsets.fromLTRB(30, 0, 30, 0), + message: AppLocalizations.of(context)!.allow_analytics_desc, + child: const Icon( + Icons.info, + ), ), - ), - const SizedBox( - width: 5, - ), - Switch( + const SizedBox( + width: 5, + ), + Switch( value: _analyticsValue!, onChanged: (value) { setState(() { _analyticsValue = value; }); AppAnalytics.setEnabled(value); - }), - ]) + }, + ), + ], + ), ], ); } @@ -123,7 +127,10 @@ class _SettingsState extends State { backgroundColor: Colors.orange[800], ), onPressed: () { - showDialog(context: context, builder: (_) => OptOutAlertDialog(subject: subject)); + showDialog( + context: context, + builder: (_) => OptOutAlertDialog(subject: subject), + ); }, ), const SizedBox(height: 24), @@ -132,9 +139,12 @@ class _SettingsState extends State { label: Text(AppLocalizations.of(context)!.delete_data), style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () { - showDialog(context: context, builder: (_) => DeleteAlertDialog(subject: subject)); + showDialog( + context: context, + builder: (_) => DeleteAlertDialog(subject: subject), + ); }, - ) + ), ], ), ), @@ -160,7 +170,11 @@ class OptOutAlertDialog extends StatelessWidget { const TextSpan(text: 'You will lose your progress in '), TextSpan( text: subject!.study.title, - style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 16), + style: TextStyle( + color: theme.primaryColor, + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), const TextSpan( text: " and won't be able to recover it. Previously completed " @@ -179,14 +193,21 @@ class OptOutAlertDialog extends StatelessWidget { await subject!.softDelete(); await deleteActiveStudyReference(); if (context.mounted) { - final studyNotifications = context.read().studyNotifications?.flutterLocalNotificationsPlugin; + final studyNotifications = context + .read() + .studyNotifications + ?.flutterLocalNotificationsPlugin; await studyNotifications?.cancelAll(); } if (context.mounted) { - Navigator.pushNamedAndRemoveUntil(context, Routes.studySelection, (_) => false); + Navigator.pushNamedAndRemoveUntil( + context, + Routes.studySelection, + (_) => false, + ); } }, - ) + ), ], ); } @@ -216,16 +237,22 @@ class DeleteAlertDialog extends StatelessWidget { await subject!.delete(); // hard-delete await deleteLocalData(); if (context.mounted) { - final studyNotifications = - context.read().studyNotifications?.flutterLocalNotificationsPlugin; + final studyNotifications = context + .read() + .studyNotifications + ?.flutterLocalNotificationsPlugin; await studyNotifications?.cancelAll(); } if (context.mounted) { - Navigator.pushNamedAndRemoveUntil(context, Routes.welcome, (_) => false); + Navigator.pushNamedAndRemoveUntil( + context, + Routes.welcome, + (_) => false, + ); } } on SocketException catch (_) {} }, - ) + ), ], ); } diff --git a/app/lib/screens/study/dashboard/task_overview_tab/progress_row.dart b/app/lib/screens/study/dashboard/task_overview_tab/progress_row.dart index 994d9cb6b..54f8d337a 100644 --- a/app/lib/screens/study/dashboard/task_overview_tab/progress_row.dart +++ b/app/lib/screens/study/dashboard/task_overview_tab/progress_row.dart @@ -2,11 +2,10 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:studyu_app/util/intervention.dart'; +import 'package:studyu_app/widgets/intervention_card.dart'; import 'package:studyu_core/core.dart'; -import '../../../../util/intervention.dart'; -import '../../../../widgets/intervention_card.dart'; - class ProgressRow extends StatefulWidget { final StudySubject? subject; @@ -21,7 +20,8 @@ class _ProgressRowState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); - final currentPhase = widget.subject!.getInterventionIndexForDate(DateTime.now()); + final currentPhase = + widget.subject!.getInterventionIndexForDate(DateTime.now()); return Padding( padding: const EdgeInsets.all(8), @@ -39,17 +39,25 @@ class _ProgressRowState extends State { indent: 5, endIndent: 5, thickness: 3, - color: currentPhase > index ? theme.primaryColor : theme.disabledColor, + color: currentPhase > index + ? theme.primaryColor + : theme.disabledColor, ), ), - widget.subject!.getInterventionsInOrder().asMap().entries.map((entry) { + widget.subject! + .getInterventionsInOrder() + .asMap() + .entries + .map((entry) { return InterventionSegment( intervention: entry.value, isCurrent: currentPhase == entry.key, isFuture: currentPhase < entry.key, phaseDuration: widget.subject!.study.schedule.phaseDuration, - percentCompleted: widget.subject!.percentCompletedForPhase(entry.key), - percentMissed: widget.subject!.percentMissedForPhase(entry.key, DateTime.now()), + percentCompleted: + widget.subject!.percentCompletedForPhase(entry.key), + percentMissed: widget.subject! + .percentMissedForPhase(entry.key, DateTime.now()), ); }), ), @@ -96,7 +104,7 @@ class InterventionSegment extends StatelessWidget { width: 8, height: 10, color: Colors.white, - ) + ), ], ), ), @@ -109,10 +117,13 @@ class InterventionSegment extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final color = isFuture ? Colors.grey : (isCurrent ? theme.colorScheme.secondary : theme.primaryColor); + final color = isFuture + ? Colors.grey + : (isCurrent ? theme.colorScheme.secondary : theme.primaryColor); final emptyColor = Color.alphaBlend(theme.dividerColor, Colors.white); - final activeColor = Color.alphaBlend(theme.colorScheme.secondary, Colors.white); + final activeColor = + Color.alphaBlend(theme.colorScheme.secondary, Colors.white); final completedColor = Color.alphaBlend(theme.primaryColor, Colors.white); return Expanded( @@ -163,7 +174,9 @@ class InterventionSegment extends StatelessWidget { }, elevation: 0, fillColor: color, - shape: const CircleBorder(side: BorderSide(color: Colors.white, width: 2)), + shape: const CircleBorder( + side: BorderSide(color: Colors.white, width: 2), + ), child: interventionIcon(intervention), ), ], @@ -172,7 +185,10 @@ class InterventionSegment extends StatelessWidget { } } -Iterable intersperseIndexed(T Function(int) generator, Iterable iterable) sync* { +Iterable intersperseIndexed( + T Function(int) generator, + Iterable iterable, +) sync* { final iterator = iterable.iterator; var index = 0; if (iterator.moveNext()) { diff --git a/app/lib/screens/study/dashboard/task_overview_tab/task_box.dart b/app/lib/screens/study/dashboard/task_overview_tab/task_box.dart index 9dbd18b3c..6b659f621 100644 --- a/app/lib/screens/study/dashboard/task_overview_tab/task_box.dart +++ b/app/lib/screens/study/dashboard/task_overview_tab/task_box.dart @@ -1,14 +1,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:studyu_app/models/app_state.dart'; +import 'package:studyu_app/screens/study/tasks/task_screen.dart'; +import 'package:studyu_app/theme.dart'; import 'package:studyu_app/util/schedule_notifications.dart'; +import 'package:studyu_app/widgets/round_checkbox.dart'; import 'package:studyu_core/core.dart'; -import '../../../../models/app_state.dart'; -import '../../../../theme.dart'; -import '../../../../widgets/round_checkbox.dart'; -import '../../tasks/task_screen.dart'; - class TaskBox extends StatefulWidget { final TaskInstance taskInstance; final Icon icon; @@ -29,7 +28,9 @@ class _TaskBoxState extends State { Future _navigateToTaskScreen() async { await Navigator.push( context, - MaterialPageRoute(builder: (context) => TaskScreen(taskInstance: widget.taskInstance)), + MaterialPageRoute( + builder: (context) => TaskScreen(taskInstance: widget.taskInstance), + ), ); widget.onCompleted(); // Rebuild widget @@ -39,17 +40,20 @@ class _TaskBoxState extends State { @override Widget build(BuildContext context) { - final completed = context - .watch() - .activeSubject! - .completedTaskInstanceForDay(widget.taskInstance.task.id, widget.taskInstance.completionPeriod, DateTime.now()); + final completed = + context.watch().activeSubject!.completedTaskInstanceForDay( + widget.taskInstance.task.id, + widget.taskInstance.completionPeriod, + DateTime.now(), + ); final isPreview = context.read().isPreview; - final isInsidePeriod = widget.taskInstance.completionPeriod.contains(StudyUTimeOfDay.now()); + final isInsidePeriod = + widget.taskInstance.completionPeriod.contains(StudyUTimeOfDay.now()); final isTaskOpen = !completed && isInsidePeriod || isPreview || kDebugMode; return Card( elevation: 2, child: InkWell( - onTap: (isTaskOpen) ? _navigateToTaskScreen : () {}, + onTap: isTaskOpen ? _navigateToTaskScreen : () {}, child: Row( children: [ Expanded( @@ -62,13 +66,14 @@ class _TaskBoxState extends State { if (isInsidePeriod || isPreview || completed) RoundCheckbox( value: completed, //_isCompleted, - onChanged: (value) => isTaskOpen ? _navigateToTaskScreen() : () {}, + onChanged: (value) => + isTaskOpen ? _navigateToTaskScreen() : () {}, ) else Padding( padding: const EdgeInsets.fromLTRB(0, 0, 8, 0), child: Icon(Icons.lock, color: theme.colorScheme.secondary), - ) + ), ], ), ), diff --git a/app/lib/screens/study/dashboard/task_overview_tab/task_overview.dart b/app/lib/screens/study/dashboard/task_overview_tab/task_overview.dart index 6c3fd9529..c43a28a49 100644 --- a/app/lib/screens/study/dashboard/task_overview_tab/task_overview.dart +++ b/app/lib/screens/study/dashboard/task_overview_tab/task_overview.dart @@ -1,20 +1,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:studyu_app/routes.dart'; +import 'package:studyu_app/screens/study/dashboard/task_overview_tab/progress_row.dart'; +import 'package:studyu_app/screens/study/dashboard/task_overview_tab/task_box.dart'; import 'package:studyu_app/theme.dart'; +import 'package:studyu_app/widgets/intervention_card.dart'; import 'package:studyu_core/core.dart'; -import '../../../../routes.dart'; -import '../../../../widgets/intervention_card.dart'; -import 'progress_row.dart'; -import 'task_box.dart'; - class TaskOverview extends StatefulWidget { final StudySubject? subject; final List? scheduleToday; final String? interventionIcon; - const TaskOverview({required this.subject, required this.scheduleToday, super.key, this.interventionIcon}); + const TaskOverview({ + required this.subject, + required this.scheduleToday, + super.key, + this.interventionIcon, + }); @override State createState() => _TaskOverviewState(); @@ -24,7 +28,11 @@ class _TaskOverviewState extends State { void _navigateToReportIfStudyCompleted(BuildContext context) { if (widget.subject!.completedStudy) { // Workaround to reload dashboard - Navigator.pushNamedAndRemoveUntil(context, Routes.dashboard, (_) => false); + Navigator.pushNamedAndRemoveUntil( + context, + Routes.dashboard, + (_) => false, + ); } } @@ -42,7 +50,8 @@ class _TaskOverviewState extends State { const SizedBox(width: 8), Text( taskInstance.completionPeriod.formatted(), - style: theme.textTheme.titleSmall!.copyWith(fontSize: 16, color: theme.primaryColor), + style: theme.textTheme.titleSmall! + .copyWith(fontSize: 16, color: theme.primaryColor), ), ], ), @@ -80,19 +89,28 @@ class _TaskOverviewState extends State { Row( children: [ Expanded( - child: - Text(AppLocalizations.of(context)!.intervention_current, style: theme.textTheme.titleLarge)), + child: Text( + AppLocalizations.of(context)!.intervention_current, + style: theme.textTheme.titleLarge, + ), + ), const Spacer(), Text( '${widget.subject!.daysLeftForPhase(widget.subject!.getInterventionIndexForDate(DateTime.now()))} ${AppLocalizations.of(context)!.days_left}', style: const TextStyle(color: primaryColor), - ) + ), ], ), const SizedBox(height: 8), - InterventionCardTitle(intervention: widget.subject!.getInterventionForDate(DateTime.now())), + InterventionCardTitle( + intervention: + widget.subject!.getInterventionForDate(DateTime.now()), + ), const SizedBox(height: 8), - Text(AppLocalizations.of(context)!.today_tasks, style: theme.textTheme.titleLarge) + Text( + AppLocalizations.of(context)!.today_tasks, + style: theme.textTheme.titleLarge, + ), ], ), ), diff --git a/app/lib/screens/study/multimodal/capture_picture_screen.dart b/app/lib/screens/study/multimodal/capture_picture_screen.dart index 28178e527..547940347 100644 --- a/app/lib/screens/study/multimodal/capture_picture_screen.dart +++ b/app/lib/screens/study/multimodal/capture_picture_screen.dart @@ -10,13 +10,18 @@ class CapturePictureScreen extends StatefulWidget { final String userId; final String studyId; - const CapturePictureScreen({super.key, required this.userId, required this.studyId}); + const CapturePictureScreen({ + super.key, + required this.userId, + required this.studyId, + }); @override State createState() => _CapturePictureScreenState(); } -class _CapturePictureScreenState extends State with WidgetsBindingObserver { +class _CapturePictureScreenState extends State + with WidgetsBindingObserver { CameraController? _cameraController; List? _cameras; int _jumpToNextCameraTaps = 0; @@ -39,7 +44,7 @@ class _CapturePictureScreenState extends State with Widget } @override - void didChangeAppLifecycleState(AppLifecycleState state) async { + Future didChangeAppLifecycleState(AppLifecycleState state) async { final CameraController? cameraController = _cameraController; // App state changed before we got the chance to initialize. @@ -85,10 +90,8 @@ class _CapturePictureScreenState extends State with Widget case 'CameraAccessDeniedWithoutPrompt': case 'CameraAccessRestricted': errorText = AppLocalizations.of(context)!.camera_access_denied; - break; case 'NoCameraAvailable': errorText = AppLocalizations.of(context)!.no_camera_available; - break; } } @@ -103,9 +106,11 @@ class _CapturePictureScreenState extends State with Widget Future> _getAvailableCameras() async { return (await availableCameras()) - .where((CameraDescription aCameraDescription) => - aCameraDescription.lensDirection == CameraLensDirection.back || - aCameraDescription.lensDirection == CameraLensDirection.front) + .where( + (CameraDescription aCameraDescription) => + aCameraDescription.lensDirection == CameraLensDirection.back || + aCameraDescription.lensDirection == CameraLensDirection.front, + ) .toList(); } @@ -116,7 +121,9 @@ class _CapturePictureScreenState extends State with Widget Future _tryCapturePicture() async { final cameraController = _cameraController; - if (cameraController == null || !cameraController.value.isInitialized || cameraController.value.isTakingPicture) { + if (cameraController == null || + !cameraController.value.isInitialized || + cameraController.value.isTakingPicture) { return; } @@ -155,59 +162,64 @@ class _CapturePictureScreenState extends State with Widget Widget build(BuildContext context) { final cameraController = _cameraController; return Scaffold( - appBar: AppBar(title: Text(AppLocalizations.of(context)!.take_a_photo)), - body: cameraController == null - ? const Center(child: CircularProgressIndicator()) - : Stack( - children: [ - CameraPreview(cameraController), - _isTakingPicture - ? Container( - color: Colors.black.withOpacity(0.5), - alignment: Alignment.center, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - borderRadius: BorderRadius.circular(10), - ), - padding: const EdgeInsets.all(20.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 16), - Text(AppLocalizations.of(context)!.take_a_photo), - ], - ), + appBar: AppBar(title: Text(AppLocalizations.of(context)!.take_a_photo)), + body: cameraController == null + ? const Center(child: CircularProgressIndicator()) + : Stack( + children: [ + CameraPreview(cameraController), + if (_isTakingPicture) + Container( + color: Colors.black.withOpacity(0.5), + alignment: Alignment.center, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).dialogBackgroundColor, + borderRadius: BorderRadius.circular(10), + ), + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.take_a_photo, ), - ) - : const SizedBox.shrink(), - ], - ), - floatingActionButton: Wrap( - direction: Axis.horizontal, - children: [ - Container( - margin: const EdgeInsets.all(10), - child: FloatingActionButton( - heroTag: "captureImage", - onPressed: cameraController != null && !_isTakingPicture - ? () async { - await _tryCapturePicture(); - } - : null, - child: const Icon(Icons.camera_alt), - ), + ], + ), + ), + ) + else + const SizedBox.shrink(), + ], ), - Container( - margin: const EdgeInsets.all(10), - child: FloatingActionButton( - heroTag: "jumpToNextCamera", - onPressed: cameraController != null && !_isTakingPicture ? () async => await _jumpToNextCamera() : null, - child: const Icon(Icons.autorenew), - ), - ) - ], - )); + floatingActionButton: Wrap( + children: [ + Container( + margin: const EdgeInsets.all(10), + child: FloatingActionButton( + heroTag: "captureImage", + onPressed: cameraController != null && !_isTakingPicture + ? () async { + await _tryCapturePicture(); + } + : null, + child: const Icon(Icons.camera_alt), + ), + ), + Container( + margin: const EdgeInsets.all(10), + child: FloatingActionButton( + heroTag: "jumpToNextCamera", + onPressed: cameraController != null && !_isTakingPicture + ? () async => await _jumpToNextCamera() + : null, + child: const Icon(Icons.autorenew), + ), + ), + ], + ), + ); } } diff --git a/app/lib/screens/study/onboarding/consent.dart b/app/lib/screens/study/onboarding/consent.dart index f297edda8..f45b2ffcb 100644 --- a/app/lib/screens/study/onboarding/consent.dart +++ b/app/lib/screens/study/onboarding/consent.dart @@ -6,15 +6,14 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:provider/provider.dart'; +import 'package:studyu_app/models/app_state.dart'; +import 'package:studyu_app/routes.dart'; +import 'package:studyu_app/screens/study/onboarding/onboarding_progress.dart'; +import 'package:studyu_app/util/save_pdf.dart'; +import 'package:studyu_app/widgets/bottom_onboarding_navigation.dart'; import 'package:studyu_app/widgets/html_text.dart'; import 'package:studyu_core/core.dart'; -import '../../../models/app_state.dart'; -import '../../../routes.dart'; -import '../../../util/save_pdf.dart'; -import '../../../widgets/bottom_onboarding_navigation.dart'; -import 'onboarding_progress.dart'; - class ConsentScreen extends StatefulWidget { const ConsentScreen({super.key}); @@ -43,15 +42,23 @@ class _ConsentScreenState extends State { } Future> generatePdfContent() async { - final ttf = pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Regular.ttf')); + final ttf = + pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Regular.ttf')); return consentList .map( (consentItem) => [ pw.Header( level: 0, - child: pw.Text(consentItem.title ?? '', textScaleFactor: 2, style: pw.TextStyle(font: ttf)), + child: pw.Text( + consentItem.title ?? '', + textScaleFactor: 2, + style: pw.TextStyle(font: ttf), + ), + ), + pw.Paragraph( + text: consentItem.description ?? '', + style: pw.TextStyle(font: ttf), ), - pw.Paragraph(text: consentItem.description ?? '', style: pw.TextStyle(font: ttf)), ], ) .expand((element) => element) @@ -75,19 +82,29 @@ class _ConsentScreenState extends State { context: context, builder: (context) => AlertDialog( elevation: 24, - title: Text(AppLocalizations.of(context)!.save_not_supported), - content: Text(AppLocalizations.of(context)!.save_not_supported_description), + title: + Text(AppLocalizations.of(context)!.save_not_supported), + content: Text( + AppLocalizations.of(context)! + .save_not_supported_description, + ), ), ); } final pdfContent = await generatePdfContent(); if (!context.mounted) return; - final savedFilePath = await savePDF(context, '${subject!.study.title}_consent', pdfContent); + final savedFilePath = await savePDF( + context, + '${subject!.study.title}_consent', + pdfContent, + ); if (savedFilePath != null) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('${AppLocalizations.of(context)!.was_saved_to}$savedFilePath.'), + content: Text( + '${AppLocalizations.of(context)!.was_saved_to}$savedFilePath.', + ), ), ); } @@ -114,23 +131,29 @@ class _ConsentScreenState extends State { style: theme.textTheme.titleMedium, ), TextSpan( - text: AppLocalizations.of(context)!.please_give_consent_why, - style: theme.textTheme.titleSmall!.copyWith(color: theme.primaryColor), + text: AppLocalizations.of(context)! + .please_give_consent_why, + style: theme.textTheme.titleSmall! + .copyWith(color: theme.primaryColor), recognizer: TapGestureRecognizer() ..onTap = () => showDialog( context: context, builder: (context) => AlertDialog( - content: Text(AppLocalizations.of(context)!.please_give_consent_reason), + content: Text( + AppLocalizations.of(context)! + .please_give_consent_reason, + ), ), ), - ) + ), ], ), ), Flexible( child: GridView.builder( shrinkWrap: true, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 10, mainAxisSpacing: 10, @@ -156,10 +179,15 @@ class _ConsentScreenState extends State { bottomNavigationBar: BottomOnboardingNavigation( backLabel: AppLocalizations.of(context)!.decline, backIcon: const Icon(Icons.close), - onBack: () => Navigator.popUntil(context, ModalRoute.withName(Routes.studySelection)), + onBack: () => Navigator.popUntil( + context, + ModalRoute.withName(Routes.studySelection), + ), nextLabel: AppLocalizations.of(context)!.accept, nextIcon: const Icon(Icons.check), - onNext: boxLogic.every((element) => element) || kDebugMode ? () => Navigator.pop(context, true) : null, + onNext: boxLogic.every((element) => element) || kDebugMode + ? () => Navigator.pop(context, true) + : null, progress: const OnboardingProgress(stage: 2, progress: 2.5), ), ); @@ -172,7 +200,13 @@ class ConsentCard extends StatelessWidget { final Function(int) onTapped; final bool? isChecked; - const ConsentCard({super.key, this.consent, this.index, required this.onTapped, this.isChecked}); + const ConsentCard({ + super.key, + this.consent, + this.index, + required this.onTapped, + this.isChecked, + }); @override Widget build(BuildContext context) { @@ -194,15 +228,22 @@ class ConsentCard extends StatelessWidget { builder: (context) => AlertDialog( title: Row( children: [ - consent!.iconName.isNotEmpty - ? Icon(MdiIcons.fromString(consent!.iconName), color: theme.primaryColor) - : const SizedBox.shrink(), - consent!.iconName.isNotEmpty ? const SizedBox(width: 8) : const SizedBox.shrink(), + if (consent!.iconName.isNotEmpty) + Icon( + MdiIcons.fromString(consent!.iconName), + color: theme.primaryColor, + ) + else + const SizedBox.shrink(), + if (consent!.iconName.isNotEmpty) + const SizedBox(width: 8) + else + const SizedBox.shrink(), Expanded(child: Text(consent!.title!)), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context), - ) + ), ], ), content: HtmlText(consent!.description), @@ -216,10 +257,18 @@ class ConsentCard extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - consent!.iconName.isNotEmpty - ? Icon(MdiIcons.fromString(consent!.iconName), size: 60, color: Colors.blue) - : const SizedBox.shrink(), - consent!.iconName.isNotEmpty ? const SizedBox(height: 10) : const SizedBox.shrink(), + if (consent!.iconName.isNotEmpty) + Icon( + MdiIcons.fromString(consent!.iconName), + size: 60, + color: Colors.blue, + ) + else + const SizedBox.shrink(), + if (consent!.iconName.isNotEmpty) + const SizedBox(height: 10) + else + const SizedBox.shrink(), Flexible( child: Text( consent!.title!, @@ -242,5 +291,10 @@ class ConsentElement { final String acknowledgmentText; final IconData icon; - ConsentElement(this.title, this.descriptionText, this.acknowledgmentText, this.icon); + ConsentElement( + this.title, + this.descriptionText, + this.acknowledgmentText, + this.icon, + ); } diff --git a/app/lib/screens/study/onboarding/eligibility_screen.dart b/app/lib/screens/study/onboarding/eligibility_screen.dart index 22bf5f07b..c0325f2b6 100644 --- a/app/lib/screens/study/onboarding/eligibility_screen.dart +++ b/app/lib/screens/study/onboarding/eligibility_screen.dart @@ -2,12 +2,11 @@ import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:studyu_app/screens/study/onboarding/onboarding_progress.dart'; +import 'package:studyu_app/widgets/bottom_onboarding_navigation.dart'; +import 'package:studyu_app/widgets/questionnaire/questionnaire_widget.dart'; import 'package:studyu_core/core.dart'; -import '../../../widgets/bottom_onboarding_navigation.dart'; -import '../../../widgets/questionnaire/questionnaire_widget.dart'; -import 'onboarding_progress.dart'; - class EligibilityResult { final bool eligible; final QuestionnaireState answers; @@ -19,7 +18,10 @@ class EligibilityResult { class EligibilityScreen extends StatefulWidget { final Study? study; - static MaterialPageRoute routeFor({required Study? study}) => MaterialPageRoute( + static MaterialPageRoute routeFor({ + required Study? study, + }) => + MaterialPageRoute( builder: (_) => EligibilityScreen(study: study), settings: const RouteSettings(name: '/eligibilityCheck'), ); @@ -48,13 +50,15 @@ class _EligibilityScreenState extends State { bool _checkContinuation(QuestionnaireState qs) { final criteria = widget.study!.eligibilityCriteria; - EligibilityCriterion? failingResult = criteria.firstWhereOrNull((element) => element.isViolated(qs)); + EligibilityCriterion? failingResult = + criteria.firstWhereOrNull((element) => element.isViolated(qs)); if (failingResult == null) return true; // freetext quickfix start failingResult = _isFreeTextCriterion(failingResult) ? null : failingResult; // freetext quickfix end setState(() { - activeResult = EligibilityResult(qs, eligible: false, firstFailed: failingResult); + activeResult = + EligibilityResult(qs, eligible: false, firstFailed: failingResult); }); return false; } @@ -73,8 +77,13 @@ class _EligibilityScreenState extends State { if (conditionResult) { activeResult = EligibilityResult(qs, eligible: conditionResult); } else { - final firstFailed = criteria.firstWhere((criterion) => criterion.isViolated(qs)); - activeResult = EligibilityResult(qs, eligible: conditionResult, firstFailed: firstFailed); + final firstFailed = + criteria.firstWhere((criterion) => criterion.isViolated(qs)); + activeResult = EligibilityResult( + qs, + eligible: conditionResult, + firstFailed: firstFailed, + ); } }); } @@ -84,7 +93,8 @@ class _EligibilityScreenState extends State { bool _isFreeTextCriterion(EligibilityCriterion criterion) { return widget.study?.questionnaire.questions.any((element) { if (criterion.condition.type == ChoiceExpression.expressionType) { - ChoiceExpression choiceExpression = criterion.condition as ChoiceExpression; + final ChoiceExpression choiceExpression = + criterion.condition as ChoiceExpression; return element.id == choiceExpression.target!; } return false; @@ -102,7 +112,10 @@ class _EligibilityScreenState extends State { color: Colors.green, size: 32, ), - content: Text(AppLocalizations.of(context)!.eligible_yes, style: Theme.of(context).textTheme.titleMedium), + content: Text( + AppLocalizations.of(context)!.eligible_yes, + style: Theme.of(context).textTheme.titleMedium, + ), actions: [Container()], forceActionsBelow: true, backgroundColor: Colors.green[50], @@ -117,13 +130,19 @@ class _EligibilityScreenState extends State { content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(AppLocalizations.of(context)!.eligible_no, style: Theme.of(context).textTheme.titleMedium), + Text( + AppLocalizations.of(context)!.eligible_no, + style: Theme.of(context).textTheme.titleMedium, + ), const SizedBox(height: 4), if (activeResult?.firstFailed?.reason != null) Text(activeResult!.firstFailed!.reason!) else const SizedBox.shrink(), - if (activeResult?.firstFailed?.reason != null) const SizedBox(height: 4) else const SizedBox.shrink(), + if (activeResult?.firstFailed?.reason != null) + const SizedBox(height: 4) + else + const SizedBox.shrink(), Text(AppLocalizations.of(context)!.eligible_mistake), ], ), @@ -131,20 +150,22 @@ class _EligibilityScreenState extends State { TextButton( onPressed: _finish, child: Text(AppLocalizations.of(context)!.eligible_back), - ) + ), ], forceActionsBelow: true, backgroundColor: Colors.red[50], ); - Widget _constructResultBanner() => activeResult!.eligible ? _constructPassBanner() : _constructFailBanner(); + Widget _constructResultBanner() => + activeResult!.eligible ? _constructPassBanner() : _constructFailBanner(); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.eligibility_questionnaire_title), + title: + Text(AppLocalizations.of(context)!.eligibility_questionnaire_title), leading: Icon(MdiIcons.clipboardList), ), body: Column( diff --git a/app/lib/screens/study/onboarding/intervention_selection.dart b/app/lib/screens/study/onboarding/intervention_selection.dart index e9c70d938..b2261a423 100644 --- a/app/lib/screens/study/onboarding/intervention_selection.dart +++ b/app/lib/screens/study/onboarding/intervention_selection.dart @@ -2,23 +2,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:provider/provider.dart'; +import 'package:studyu_app/models/app_state.dart'; +import 'package:studyu_app/routes.dart'; +import 'package:studyu_app/screens/study/onboarding/onboarding_progress.dart'; +import 'package:studyu_app/widgets/bottom_onboarding_navigation.dart'; +import 'package:studyu_app/widgets/intervention_card.dart'; import 'package:studyu_core/core.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import '../../../models/app_state.dart'; -import '../../../routes.dart'; -import '../../../widgets/bottom_onboarding_navigation.dart'; -import '../../../widgets/intervention_card.dart'; -import 'onboarding_progress.dart'; - class InterventionSelectionScreen extends StatefulWidget { const InterventionSelectionScreen({super.key}); @override - State createState() => _InterventionSelectionScreenState(); + State createState() => + _InterventionSelectionScreenState(); } -class _InterventionSelectionScreenState extends State { +class _InterventionSelectionScreenState + extends State { final List selectedInterventionIds = []; Study? selectedStudy; @@ -39,8 +40,10 @@ class _InterventionSelectionScreenState extends State interventionId == interventions[index].id), + selected: selectedInterventionIds.any( + (interventionId) => interventionId == interventions[index].id, + ), onTap: () => onSelect(interventions[index].id), ), ), @@ -73,7 +78,9 @@ class _InterventionSelectionScreenState extends State 2) selectedInterventionIds.removeAt(0); + if (selectedInterventionIds.length > 2) { + selectedInterventionIds.removeAt(0); + } } else { selectedInterventionIds.removeWhere((id) => id == interventionId); } @@ -116,7 +123,10 @@ class _InterventionSelectionScreenState extends State { } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(AppLocalizations.of(context)!.user_did_not_give_consent), + content: + Text(AppLocalizations.of(context)!.user_did_not_give_consent), duration: const Duration(seconds: 30), ), ); @@ -94,8 +94,12 @@ class Timeline extends StatelessWidget { return InterventionTile( title: intervention.name, iconName: intervention.icon, - color: intervention.isBaseline() ? Colors.grey : theme.colorScheme.secondary, - date: now.add(Duration(days: index * subject!.study.schedule.phaseDuration)), + color: intervention.isBaseline() + ? Colors.grey + : theme.colorScheme.secondary, + date: now.add( + Duration(days: index * subject!.study.schedule.phaseDuration), + ), isFirst: index == 0, ); }), @@ -105,7 +109,7 @@ class Timeline extends StatelessWidget { color: Colors.green, isLast: true, date: subject!.endDate(now), - ) + ), ], ); } @@ -146,10 +150,17 @@ class InterventionTile extends StatelessWidget { beforeLineStyle: LineStyle(color: theme.primaryColor), afterLineStyle: LineStyle(color: theme.primaryColor), endChild: TimelineChild( - child: Text(title!, style: theme.textTheme.titleLarge!.copyWith(color: theme.primaryColor)), + child: Text( + title!, + style: + theme.textTheme.titleLarge!.copyWith(color: theme.primaryColor), + ), ), startChild: TimelineChild( - child: Text(DateFormat('dd-MM-yyyy').format(date), style: const TextStyle(fontWeight: FontWeight.bold)), + child: Text( + DateFormat('dd-MM-yyyy').format(date), + style: const TextStyle(fontWeight: FontWeight.bold), + ), ), ); } @@ -164,7 +175,10 @@ class IconIndicator extends StatelessWidget { @override Widget build(BuildContext context) { return DecoratedBox( - decoration: BoxDecoration(shape: BoxShape.circle, color: color ?? Theme.of(context).colorScheme.secondary), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color ?? Theme.of(context).colorScheme.secondary, + ), child: Center( child: Icon(MdiIcons.fromString(iconName), color: Colors.white), ), diff --git a/app/lib/screens/study/onboarding/kickoff.dart b/app/lib/screens/study/onboarding/kickoff.dart index b46b652aa..d59c681a9 100644 --- a/app/lib/screens/study/onboarding/kickoff.dart +++ b/app/lib/screens/study/onboarding/kickoff.dart @@ -2,13 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:provider/provider.dart'; +import 'package:studyu_app/models/app_state.dart'; +import 'package:studyu_app/routes.dart'; import 'package:studyu_app/util/cache.dart'; import 'package:studyu_core/core.dart'; import 'package:studyu_flutter_common/studyu_flutter_common.dart'; -import '../../../models/app_state.dart'; -import '../../../routes.dart'; - class KickoffScreen extends StatefulWidget { const KickoffScreen({super.key}); @@ -33,7 +32,11 @@ class _KickoffScreen extends State { await storeActiveSubjectId(subject!.id); if (!context.mounted) return; setState(() => ready = true); - Navigator.pushNamedAndRemoveUntil(context, Routes.dashboard, (_) => false); + Navigator.pushNamedAndRemoveUntil( + context, + Routes.dashboard, + (_) => false, + ); } catch (e) { StudyULogger.fatal('Failed creating subject: $e'); } @@ -58,8 +61,9 @@ class _KickoffScreen extends State { size: 64, ); - String _getStatusText(BuildContext context) => - !ready ? AppLocalizations.of(context)!.setting_up_study : AppLocalizations.of(context)!.good_to_go; + String _getStatusText(BuildContext context) => !ready + ? AppLocalizations.of(context)!.setting_up_study + : AppLocalizations.of(context)!.good_to_go; @override Widget build(BuildContext context) { diff --git a/app/lib/screens/study/onboarding/onboarding_progress.dart b/app/lib/screens/study/onboarding/onboarding_progress.dart index 97556a10b..12bfb89fa 100644 --- a/app/lib/screens/study/onboarding/onboarding_progress.dart +++ b/app/lib/screens/study/onboarding/onboarding_progress.dart @@ -4,7 +4,11 @@ class OnboardingProgress extends StatelessWidget { final int stage; final double progress; - const OnboardingProgress({required this.stage, required this.progress, super.key}); + const OnboardingProgress({ + required this.stage, + required this.progress, + super.key, + }); double _getProgressForStage(int stage) { if (stage < this.stage) return 1; diff --git a/app/lib/screens/study/onboarding/study_overview.dart b/app/lib/screens/study/onboarding/study_overview.dart index 16d4148e9..732aa6ae1 100644 --- a/app/lib/screens/study/onboarding/study_overview.dart +++ b/app/lib/screens/study/onboarding/study_overview.dart @@ -2,16 +2,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:provider/provider.dart'; +import 'package:studyu_app/models/app_state.dart'; +import 'package:studyu_app/routes.dart'; +import 'package:studyu_app/screens/study/dashboard/contact_tab/contact_screen.dart'; +import 'package:studyu_app/screens/study/onboarding/eligibility_screen.dart'; +import 'package:studyu_app/widgets/bottom_onboarding_navigation.dart'; +import 'package:studyu_app/widgets/study_tile.dart'; import 'package:studyu_core/core.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import '../../../models/app_state.dart'; -import '../../../routes.dart'; -import '../../../widgets/bottom_onboarding_navigation.dart'; -import '../../../widgets/study_tile.dart'; -import '../dashboard/contact_tab/contact_screen.dart'; -import 'eligibility_screen.dart'; - class StudyOverviewScreen extends StatefulWidget { const StudyOverviewScreen({super.key}); @@ -54,7 +53,10 @@ class _StudyOverviewScreen extends State { Future navigateToEligibilityCheck(BuildContext context) async { final study = context.read().selectedStudy; - final result = await Navigator.push(context, EligibilityScreen.routeFor(study: study)); + final result = await Navigator.push( + context, + EligibilityScreen.routeFor(study: study), + ); if (result == null) return; if (!context.mounted) return; @@ -103,20 +105,31 @@ class StudyDetailsView extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final baselineLength = study!.schedule.includeBaseline ? study!.schedule.phaseDuration : 0; + final baselineLength = + study!.schedule.includeBaseline ? study!.schedule.phaseDuration : 0; final studyLength = baselineLength + - study!.schedule.phaseDuration * study!.schedule.numberOfCycles * StudySchedule.numberOfInterventions; + study!.schedule.phaseDuration * + study!.schedule.numberOfCycles * + StudySchedule.numberOfInterventions; return Column( children: [ ListTile( - title: Text(AppLocalizations.of(context)!.intervention_phase_duration), - subtitle: Text('${study!.schedule.phaseDuration} ${AppLocalizations.of(context)!.days}'), - leading: Icon(MdiIcons.clock, color: theme.primaryColor, size: iconSize), + title: + Text(AppLocalizations.of(context)!.intervention_phase_duration), + subtitle: Text( + '${study!.schedule.phaseDuration} ${AppLocalizations.of(context)!.days}', + ), + leading: + Icon(MdiIcons.clock, color: theme.primaryColor, size: iconSize), ), ListTile( title: Text(AppLocalizations.of(context)!.study_length), subtitle: Text('$studyLength ${AppLocalizations.of(context)!.days}'), - leading: Icon(MdiIcons.calendar, color: theme.primaryColor, size: iconSize), + leading: Icon( + MdiIcons.calendar, + color: theme.primaryColor, + size: iconSize, + ), ), const SizedBox(height: 16), ContactWidget( diff --git a/app/lib/screens/study/onboarding/study_selection.dart b/app/lib/screens/study/onboarding/study_selection.dart index 35f072a74..8a4899540 100644 --- a/app/lib/screens/study/onboarding/study_selection.dart +++ b/app/lib/screens/study/onboarding/study_selection.dart @@ -5,15 +5,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:provider/provider.dart'; +import 'package:studyu_app/models/app_state.dart'; +import 'package:studyu_app/routes.dart'; +import 'package:studyu_app/widgets/bottom_onboarding_navigation.dart'; +import 'package:studyu_app/widgets/study_tile.dart'; import 'package:studyu_core/core.dart'; import 'package:studyu_flutter_common/studyu_flutter_common.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import '../../../models/app_state.dart'; -import '../../../routes.dart'; -import '../../../widgets/bottom_onboarding_navigation.dart'; -import '../../../widgets/study_tile.dart'; - Future navigateToStudyOverview( BuildContext context, Study study, { @@ -30,7 +29,8 @@ Future showAppOutdatedDialog(BuildContext context) async { await showDialog( context: context, builder: (context) => AlertDialog( - title: Text(AppLocalizations.of(context)!.study_selection_unsupported_title), + title: + Text(AppLocalizations.of(context)!.study_selection_unsupported_title), content: Text(AppLocalizations.of(context)!.study_selection_unsupported), actions: [ TextButton( @@ -74,7 +74,8 @@ class _StudySelectionScreenState extends State { text: TextSpan( children: [ TextSpan( - text: AppLocalizations.of(context)!.study_selection_single, + text: AppLocalizations.of(context)! + .study_selection_single, style: theme.textTheme.titleSmall, ), TextSpan( @@ -82,52 +83,64 @@ class _StudySelectionScreenState extends State { style: theme.textTheme.titleSmall, ), TextSpan( - text: AppLocalizations.of(context)!.study_selection_single_why, - style: theme.textTheme.titleSmall!.copyWith(color: theme.primaryColor), + text: AppLocalizations.of(context)! + .study_selection_single_why, + style: theme.textTheme.titleSmall! + .copyWith(color: theme.primaryColor), recognizer: TapGestureRecognizer() ..onTap = () => showDialog( context: context, builder: (context) => AlertDialog( - content: Text(AppLocalizations.of(context)!.study_selection_single_reason), + content: Text( + AppLocalizations.of(context)! + .study_selection_single_reason, + ), ), ), - ) + ), ], ), ), ], ), ), - _hiddenStudies - ? Column( - children: [ - MaterialBanner( - padding: const EdgeInsets.all(8), - leading: Icon( - MdiIcons.exclamationThick, - color: Colors.orange, - size: 32, - ), - content: Text( - AppLocalizations.of(context)!.study_selection_hidden_studies, - style: Theme.of(context).textTheme.titleSmall, - ), - actions: const [SizedBox.shrink()], - backgroundColor: Colors.yellow[100], - ), - const SizedBox(height: 16), - ], - ) - : const SizedBox.shrink(), + if (_hiddenStudies) + Column( + children: [ + MaterialBanner( + padding: const EdgeInsets.all(8), + leading: Icon( + MdiIcons.exclamationThick, + color: Colors.orange, + size: 32, + ), + content: Text( + AppLocalizations.of(context)! + .study_selection_hidden_studies, + style: Theme.of(context).textTheme.titleSmall, + ), + actions: const [SizedBox.shrink()], + backgroundColor: Colors.yellow[100], + ), + const SizedBox(height: 16), + ], + ) + else + const SizedBox.shrink(), Expanded( child: RetryFutureBuilder>( tryFunction: () async => publishedStudies, - successBuilder: (BuildContext context, ExtractionResult? extractionResult) { + successBuilder: ( + BuildContext context, + ExtractionResult? extractionResult, + ) { final studies = extractionResult!.extracted; if (extractionResult is ExtractionFailedException) { WidgetsBinding.instance.addPostFrameCallback((_) { if (_hiddenStudies) return; - debugPrint('${extractionResult.notExtracted.length} studies could not be extracted.'); + debugPrint( + '${extractionResult.notExtracted.length} studies could not be extracted.', + ); setState(() { _hiddenStudies = true; }); @@ -158,7 +171,10 @@ class _StudySelectionScreenState extends State { child: OutlinedButton.icon( icon: Icon(MdiIcons.key), onPressed: () async { - await showDialog(context: context, builder: (_) => const InviteCodeDialog()); + await showDialog( + context: context, + builder: (_) => const InviteCodeDialog(), + ); }, label: Text(AppLocalizations.of(context)!.invite_code_button), ), @@ -198,7 +214,9 @@ class _InviteCodeDialogState extends State { controller: _controller, validator: (_) => _errorMessage, autovalidateMode: AutovalidateMode.always, - decoration: InputDecoration(labelText: AppLocalizations.of(context)!.invite_code), + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.invite_code, + ), ), actions: [ OutlinedButton.icon( @@ -223,7 +241,8 @@ class _InviteCodeDialogState extends State { if (result == null) { setState(() { - _errorMessage = AppLocalizations.of(context)!.invalid_invite_code; + _errorMessage = + AppLocalizations.of(context)!.invalid_invite_code; }); } else { setState(() { @@ -232,10 +251,10 @@ class _InviteCodeDialogState extends State { Map? studyResult; try { - studyResult = await (Supabase.instance.client.rpc( + studyResult = await Supabase.instance.client.rpc( 'get_study_record_from_invite', params: {'invite_code': _controller.text}, - ).single()); + ).single(); } on PostgrestException catch (error) { print(error.message); setState(() { @@ -281,7 +300,7 @@ class _InviteCodeDialogState extends State { } } }, - ) + ), ], ); } diff --git a/app/lib/screens/study/report/disclaimer_section.dart b/app/lib/screens/study/report/disclaimer_section.dart index 16950dfe3..f13956207 100644 --- a/app/lib/screens/study/report/disclaimer_section.dart +++ b/app/lib/screens/study/report/disclaimer_section.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'generic_section.dart'; +import 'package:studyu_app/screens/study/report/generic_section.dart'; class DisclaimerSection extends GenericSection { const DisclaimerSection(super.subject, {super.key, super.onTap}); diff --git a/app/lib/screens/study/report/general_details_section.dart b/app/lib/screens/study/report/general_details_section.dart index cb9bd8ab9..6bc44226f 100644 --- a/app/lib/screens/study/report/general_details_section.dart +++ b/app/lib/screens/study/report/general_details_section.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; - -import '../../../widgets/study_tile.dart'; -import 'generic_section.dart'; +import 'package:studyu_app/screens/study/report/generic_section.dart'; +import 'package:studyu_app/widgets/study_tile.dart'; class GeneralDetailsSection extends GenericSection { const GeneralDetailsSection(super.subject, {super.key, super.onTap}); diff --git a/app/lib/screens/study/report/performance/performance_details.dart b/app/lib/screens/study/report/performance/performance_details.dart index ed9c71fe8..28b5210ca 100644 --- a/app/lib/screens/study/report/performance/performance_details.dart +++ b/app/lib/screens/study/report/performance/performance_details.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:studyu_app/routes.dart'; +import 'package:studyu_app/widgets/intervention_card.dart'; import 'package:studyu_core/core.dart'; -import '../../../../routes.dart'; -import '../../../../widgets/intervention_card.dart'; - class PerformanceDetailsScreen extends StatelessWidget { final StudySubject? reportSubject; - static MaterialPageRoute routeFor({required StudySubject? subject}) => MaterialPageRoute( + static MaterialPageRoute routeFor({required StudySubject? subject}) => + MaterialPageRoute( builder: (_) => PerformanceDetailsScreen(subject), settings: const RouteSettings(name: Routes.performanceDetails), ); @@ -18,8 +18,9 @@ class PerformanceDetailsScreen extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final interventions = - reportSubject!.selectedInterventions.where((intervention) => !intervention.isBaseline()).toList(); + final interventions = reportSubject!.selectedInterventions + .where((intervention) => !intervention.isBaseline()) + .toList(); return Scaffold( appBar: AppBar( @@ -35,15 +36,20 @@ class PerformanceDetailsScreen extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.all(8), - child: Text(AppLocalizations.of(context)!.performance_overview, style: theme.textTheme.titleMedium), + child: Text( + AppLocalizations.of(context)!.performance_overview, + style: theme.textTheme.titleMedium, + ), ), Padding( padding: const EdgeInsets.all(8), child: Align( alignment: Alignment.centerLeft, child: Text( - AppLocalizations.of(context)!.performance_overview_interventions, - style: theme.textTheme.titleLarge!.copyWith(color: theme.primaryColor), + AppLocalizations.of(context)! + .performance_overview_interventions, + style: theme.textTheme.titleLarge! + .copyWith(color: theme.primaryColor), ), ), ), @@ -51,16 +57,20 @@ class PerformanceDetailsScreen extends StatelessWidget { physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemCount: interventions.length, - itemBuilder: (context, index) => - InterventionPerformanceBar(subject: reportSubject, intervention: interventions[index]), + itemBuilder: (context, index) => InterventionPerformanceBar( + subject: reportSubject, + intervention: interventions[index], + ), ), Padding( padding: const EdgeInsets.all(8), child: Align( alignment: Alignment.centerLeft, child: Text( - AppLocalizations.of(context)!.performance_overview_observations, - style: theme.textTheme.titleLarge!.copyWith(color: theme.primaryColor), + AppLocalizations.of(context)! + .performance_overview_observations, + style: theme.textTheme.titleLarge! + .copyWith(color: theme.primaryColor), ), ), ), @@ -86,7 +96,11 @@ class InterventionPerformanceBar extends StatelessWidget { final Intervention intervention; final StudySubject? subject; - const InterventionPerformanceBar({required this.intervention, required this.subject, super.key}); + const InterventionPerformanceBar({ + required this.intervention, + required this.subject, + super.key, + }); @override Widget build(BuildContext context) { @@ -95,7 +109,11 @@ class InterventionPerformanceBar extends StatelessWidget { padding: const EdgeInsets.all(16), child: Column( children: [ - InterventionCard(intervention, showTasks: false, showDescription: false), + InterventionCard( + intervention, + showTasks: false, + showDescription: false, + ), const SizedBox(height: 8), ...intervention.tasks.map( (task) => PerformanceBar( @@ -103,7 +121,7 @@ class InterventionPerformanceBar extends StatelessWidget { completed: subject!.completedTasksFor(task), total: subject!.totalTaskCountFor(task), ), - ) + ), ], ), ), @@ -115,7 +133,11 @@ class ObservationPerformanceBar extends StatelessWidget { final Observation observation; final StudySubject? subject; - const ObservationPerformanceBar({required this.observation, required this.subject, super.key}); + const ObservationPerformanceBar({ + required this.observation, + required this.subject, + super.key, + }); @override Widget build(BuildContext context) { @@ -137,7 +159,12 @@ class PerformanceBar extends StatelessWidget { final int completed; final int total; - const PerformanceBar({required this.task, required this.completed, required this.total, super.key}); + const PerformanceBar({ + required this.task, + required this.completed, + required this.total, + super.key, + }); @override Widget build(BuildContext context) { @@ -163,9 +190,9 @@ class PerformanceBar extends StatelessWidget { '${(completed / total * 100).toStringAsFixed(2).replaceAll('.00', '')} %', style: const TextStyle(fontWeight: FontWeight.bold), ), - ) + ), ], - ) + ), ], ); } diff --git a/app/lib/screens/study/report/performance/performance_section.dart b/app/lib/screens/study/report/performance/performance_section.dart index 41dec298b..da07b1105 100644 --- a/app/lib/screens/study/report/performance/performance_section.dart +++ b/app/lib/screens/study/report/performance/performance_section.dart @@ -3,10 +3,9 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:rainbow_color/rainbow_color.dart'; +import 'package:studyu_app/screens/study/report/generic_section.dart'; import 'package:studyu_core/core.dart'; -import '../generic_section.dart'; - class PerformanceSection extends GenericSection { const PerformanceSection(super.subject, {super.key, super.onTap}); @@ -17,13 +16,19 @@ class PerformanceSection extends GenericSection { @override Widget buildContent(BuildContext context) { - final interventions = - subject!.selectedInterventions.where((intervention) => intervention.id != Study.baselineID).toList(); + final interventions = subject!.selectedInterventions + .where((intervention) => intervention.id != Study.baselineID) + .toList(); final interventionProgress = interventions.map((intervention) { - final countableInterventions = getCountableObservationAmount(intervention); - return min(countableInterventions == 0 ? 0 : countableInterventions / maximum, 1); + final countableInterventions = + getCountableObservationAmount(intervention); + return min( + countableInterventions == 0 ? 0 : countableInterventions / maximum, + 1, + ); }).toList(); - return interventions.length != 2 || subject!.study.reportSpecification.primary == null + return interventions.length != 2 || + subject!.study.reportSpecification.primary == null ? Center( child: Text(AppLocalizations.of(context)!.performance), ) @@ -64,7 +69,10 @@ class PerformanceSection extends GenericSection { ); } - String getPowerLevelDescription(BuildContext context, List interventionProgress) { + String getPowerLevelDescription( + BuildContext context, + List interventionProgress, + ) { if (interventionProgress.any((progress) => progress < minimumRatio)) { return AppLocalizations.of(context)!.not_enough_data; } else if (interventionProgress.any((progress) => progress < 1)) { @@ -81,13 +89,23 @@ class PerformanceSection extends GenericSection { } var countable = 0; - subject!.getResultsByDate(interventionId: intervention.id).values.forEach((progress) { + subject! + .getResultsByDate(interventionId: intervention.id) + .values + .forEach((progress) { if (progress - .where((result) => intervention.tasks.any((interventionTask) => interventionTask.id == result.taskId)) + .where( + (result) => intervention.tasks.any( + (interventionTask) => interventionTask.id == result.taskId, + ), + ) .length == interventionsPerDay) { countable += progress - .where((result) => subject!.study.observations.any((observation) => observation.id == result.taskId)) + .where( + (result) => subject!.study.observations + .any((observation) => observation.id == result.taskId), + ) .length; } }); @@ -126,12 +144,18 @@ class PerformanceBar extends StatelessWidget { @override Widget build(BuildContext context) { - final rainbow = Rainbow(spectrum: [Colors.red, Colors.yellow, Colors.green], rangeStart: 0, rangeEnd: 1); + final rainbow = Rainbow( + spectrum: [Colors.red, Colors.yellow, Colors.green], + rangeStart: 0, + rangeEnd: 1, + ); final fullSpectrum = List.generate(3, (index) => index * 0.5) .map((index) => rainbow[index].withOpacity(0.4)) .toList(); final colorSamples = - List.generate(11, (index) => index * 0.1 * progress).map((index) => rainbow[index]).toList(); + List.generate(11, (index) => index * 0.1 * progress) + .map((index) => rainbow[index]) + .toList(); final spacing = (minimum! * 1000).floor(); @@ -172,7 +196,7 @@ class PerformanceBar extends StatelessWidget { Container( width: 2, color: Colors.grey[600], - ) + ), ], ), ], diff --git a/app/lib/screens/study/report/report_details.dart b/app/lib/screens/study/report/report_details.dart index 5ae98fde1..32bf3b03e 100644 --- a/app/lib/screens/study/report/report_details.dart +++ b/app/lib/screens/study/report/report_details.dart @@ -1,19 +1,19 @@ import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:studyu_app/routes.dart'; +import 'package:studyu_app/screens/study/report/disclaimer_section.dart'; +import 'package:studyu_app/screens/study/report/general_details_section.dart'; +import 'package:studyu_app/screens/study/report/performance/performance_details.dart'; +import 'package:studyu_app/screens/study/report/performance/performance_section.dart'; +import 'package:studyu_app/screens/study/report/report_section_container.dart'; import 'package:studyu_core/core.dart'; -import '../../../routes.dart'; -import 'disclaimer_section.dart'; -import 'general_details_section.dart'; -import 'performance/performance_details.dart'; -import 'performance/performance_section.dart'; -import 'report_section_container.dart'; - class ReportDetailsScreen extends StatelessWidget { final StudySubject subject; - static MaterialPageRoute routeFor({required StudySubject subject}) => MaterialPageRoute( + static MaterialPageRoute routeFor({required StudySubject subject}) => + MaterialPageRoute( builder: (_) => ReportDetailsScreen(subject), settings: const RouteSettings(name: Routes.reportDetails), ); @@ -43,17 +43,23 @@ class ReportDetailsScreen extends StatelessWidget { DisclaimerSection(subject), PerformanceSection( subject, - onTap: () => Navigator.push(context, PerformanceDetailsScreen.routeFor(subject: subject)), + onTap: () => Navigator.push( + context, + PerformanceDetailsScreen.routeFor(subject: subject), + ), ), - if (subject.study.reportSpecification.primary != null && (subject.completedStudy || kDebugMode)) + if (subject.study.reportSpecification.primary != null && + (subject.completedStudy || kDebugMode)) ReportSectionContainer( subject.study.reportSpecification.primary!, subject: subject, primary: true, ), - if (subject.study.reportSpecification.secondary.isNotEmpty && (subject.completedStudy || kDebugMode)) - ...subject.study.reportSpecification.secondary - .map((section) => ReportSectionContainer(section, subject: subject)) + if (subject.study.reportSpecification.secondary.isNotEmpty && + (subject.completedStudy || kDebugMode)) + ...subject.study.reportSpecification.secondary.map( + (section) => ReportSectionContainer(section, subject: subject), + ), ], ), ), diff --git a/app/lib/screens/study/report/report_history.dart b/app/lib/screens/study/report/report_history.dart index 7fcd689e7..2250a7346 100644 --- a/app/lib/screens/study/report/report_history.dart +++ b/app/lib/screens/study/report/report_history.dart @@ -2,13 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:provider/provider.dart'; +import 'package:studyu_app/models/app_state.dart'; +import 'package:studyu_app/screens/study/report/report_details.dart'; import 'package:studyu_core/core.dart'; import 'package:studyu_flutter_common/studyu_flutter_common.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import '../../../models/app_state.dart'; -import 'report_details.dart'; - class ReportHistoryScreen extends StatelessWidget { const ReportHistoryScreen({super.key}); @@ -21,8 +20,11 @@ class ReportHistoryScreen extends StatelessWidget { ), ), body: RetryFutureBuilder>( - tryFunction: () => StudySubject.getStudyHistory(Supabase.instance.client.auth.currentUser!.id), - successBuilder: (BuildContext context, List? pastStudies) { + tryFunction: () => StudySubject.getStudyHistory( + Supabase.instance.client.auth.currentUser!.id, + ), + successBuilder: + (BuildContext context, List? pastStudies) { return ListView.builder( itemCount: pastStudies!.length, itemBuilder: (context, index) { @@ -49,7 +51,10 @@ class ReportHistoryItem extends StatelessWidget { color: isActiveStudy ? Colors.green[600] : theme.cardColor, child: InkWell( onTap: () { - Navigator.push(context, ReportDetailsScreen.routeFor(subject: subject)); + Navigator.push( + context, + ReportDetailsScreen.routeFor(subject: subject), + ); }, child: Padding( padding: const EdgeInsets.all(20), @@ -58,14 +63,17 @@ class ReportHistoryItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Icon( - MdiIcons.fromString(subject.study.iconName) ?? MdiIcons.accountHeart, + MdiIcons.fromString(subject.study.iconName) ?? + MdiIcons.accountHeart, color: isActiveStudy ? Colors.white : Colors.black, ), const SizedBox(width: 16), Expanded( child: Text( subject.study.title!, - style: theme.textTheme.headlineSmall!.copyWith(color: isActiveStudy ? Colors.white : Colors.black), + style: theme.textTheme.headlineSmall!.copyWith( + color: isActiveStudy ? Colors.white : Colors.black, + ), ), ), ], diff --git a/app/lib/screens/study/report/report_section_container.dart b/app/lib/screens/study/report/report_section_container.dart index ef8bfb5ca..df4539b35 100644 --- a/app/lib/screens/study/report/report_section_container.dart +++ b/app/lib/screens/study/report/report_section_container.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:studyu_app/screens/study/report/report_section_widget.dart'; import 'package:studyu_app/screens/study/report/sections/average_section_widget.dart'; import 'package:studyu_app/screens/study/report/sections/linear_regression_section_widget.dart'; import 'package:studyu_core/core.dart'; -import 'report_section_widget.dart'; - -typedef SectionBuilder = ReportSectionWidget Function(ReportSection section, StudySubject subject); +typedef SectionBuilder = ReportSectionWidget Function( + ReportSection section, + StudySubject subject, +); class ReportSectionContainer extends StatelessWidget { final ReportSection section; @@ -14,11 +16,18 @@ class ReportSectionContainer extends StatelessWidget { final bool primary; final GestureTapCallback? onTap; - const ReportSectionContainer(this.section, {super.key, required this.subject, this.onTap, this.primary = false}); + const ReportSectionContainer( + this.section, { + super.key, + required this.subject, + this.onTap, + this.primary = false, + }); ReportSectionWidget buildContents(BuildContext context) => switch (section) { - AverageSection averageSection => AverageSectionWidget(subject, averageSection), - LinearRegressionSection linearRegressionSection => + final AverageSection averageSection => + AverageSectionWidget(subject, averageSection), + final LinearRegressionSection linearRegressionSection => LinearRegressionSectionWidget(subject, linearRegressionSection), _ => throw ArgumentError('Section type ${section.type} not supported.'), }; @@ -26,7 +35,8 @@ class ReportSectionContainer extends StatelessWidget { List buildPrimaryHeader(BuildContext context, ThemeData theme) => [ Text( AppLocalizations.of(context)!.report_primary_result.toUpperCase(), - style: theme.textTheme.labelSmall!.copyWith(color: theme.colorScheme.secondary), + style: theme.textTheme.labelSmall! + .copyWith(color: theme.colorScheme.secondary), ), const SizedBox(height: 4), ]; diff --git a/app/lib/screens/study/report/sections/average_section_widget.dart b/app/lib/screens/study/report/sections/average_section_widget.dart index ad68f675f..bf949f7d7 100644 --- a/app/lib/screens/study/report/sections/average_section_widget.dart +++ b/app/lib/screens/study/report/sections/average_section_widget.dart @@ -1,14 +1,13 @@ import 'package:collection/collection.dart'; import 'package:fl_chart/fl_chart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:studyu_app/screens/study/report/report_section_widget.dart'; +import 'package:studyu_app/screens/study/report/util/plot_utilities.dart'; import 'package:studyu_app/theme.dart'; +import 'package:studyu_app/util/data_processing.dart'; import 'package:studyu_core/core.dart'; -import '../../../../util/data_processing.dart'; -import '../report_section_widget.dart'; -import '../util/plot_utilities.dart'; - class AverageSectionWidget extends ReportSectionWidget { final AverageSection section; @@ -17,13 +16,21 @@ class AverageSectionWidget extends ReportSectionWidget { @override Widget build(BuildContext context) { final data = getAggregatedData().toList(); - final taskTitle = - subject.study.observations.firstWhereOrNull((element) => element.id == section.resultProperty!.task)?.title; + final taskTitle = subject.study.observations + .firstWhereOrNull( + (element) => element.id == section.resultProperty!.task, + ) + ?.title; return Column( mainAxisSize: MainAxisSize.min, children: [ - if (taskTitle != null) Text(taskTitle, style: theme.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.bold)), + if (taskTitle != null) + Text( + taskTitle, + style: theme.textTheme.bodyLarge! + .copyWith(fontWeight: FontWeight.bold), + ), const SizedBox(height: 8), getLegend(context, data), const SizedBox(height: 8), @@ -36,21 +43,27 @@ class AverageSectionWidget extends ReportSectionWidget { final numberOfPhases = subject.interventionOrder.length; final phaseDuration = subject.study.schedule.phaseDuration; return Iterable.generate(numberOfPhases) - .map((i) => (((i + 1) * phaseDuration - ((phaseDuration / 2) - 1)) - 1).floor()) + .map( + (i) => (((i + 1) * phaseDuration - ((phaseDuration / 2) - 1)) - 1) + .floor(), + ) .toList(); } List get phasePos { final numberOfPhases = subject.interventionOrder.length; final phaseDuration = subject.study.schedule.phaseDuration; - return Iterable.generate(numberOfPhases).map((i) => (i + 1) * phaseDuration).toList(); + return Iterable.generate(numberOfPhases) + .map((i) => (i + 1) * phaseDuration) + .toList(); } Widget getLegend(BuildContext context, List data) { final interventionNames = getInterventionNames(context); final legends = { - for (var entry in data) - interventionNames[entry.intervention]!: Legend(interventionNames[entry.intervention]!, getColor(entry)) + for (final entry in data) + interventionNames[entry.intervention]!: + Legend(interventionNames[entry.intervention]!, getColor(entry)), }; return LegendsListWidget(legends: legends.values.toList()); } @@ -58,27 +71,30 @@ class AverageSectionWidget extends ReportSectionWidget { Widget getDiagram(BuildContext context, List data) { return BarChart( getChartData(context, data), - swapAnimationDuration: const Duration(milliseconds: 150), // Optional - swapAnimationCurve: Curves.linear, // Optional ); } BarChartData getChartData(BuildContext context, List data) { final barGroups = getBarGroups(context, data); - final maxY = ((data.sortedBy((entry) => entry.value).toList().lastOrNull?.value ?? 0) * 1.1).ceilToDouble(); + final maxY = + ((data.sortedBy((entry) => entry.value).toList().lastOrNull?.value ?? + 0) * + 1.1) + .ceilToDouble(); return BarChartData( titlesData: FlTitlesData( - bottomTitles: AxisTitles( - axisNameWidget: - (section.aggregate != TemporalAggregation.intervention) ? const Text("Phase") : const Text(""), - sideTitles: SideTitles( - showTitles: true, - getTitlesWidget: getTitles, - )), - topTitles: const AxisTitles( - sideTitles: SideTitles( - showTitles: false, - ))), + bottomTitles: AxisTitles( + axisNameWidget: + (section.aggregate != TemporalAggregation.intervention) + ? const Text("Phase") + : const Text(""), + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: getTitles, + ), + ), + topTitles: const AxisTitles(), + ), gridData: getGridData(barGroups), alignment: BarChartAlignment.spaceAround, barGroups: barGroups, @@ -112,7 +128,10 @@ class AverageSectionWidget extends ReportSectionWidget { } } - List getBarGroups(BuildContext context, List data) { + List getBarGroups( + BuildContext context, + List data, + ) { if (data.isEmpty) return [BarChartGroupData(x: 0)]; int barCount = 0; @@ -122,7 +141,8 @@ class AverageSectionWidget extends ReportSectionWidget { case TemporalAggregation.phase: barCount = subject.interventionOrder.length; case TemporalAggregation.intervention: - barCount = subject.selectedInterventionIds.length + (subject.study.schedule.includeBaseline ? 1 : 0); + barCount = subject.selectedInterventionIds.length + + (subject.study.schedule.includeBaseline ? 1 : 0); default: } @@ -142,9 +162,13 @@ class AverageSectionWidget extends ReportSectionWidget { return BarChartGroupData(x: index, barsSpace: 0, barRods: [rod]); } - var starter = List.generate(barCount, barGenerator); - for (var entry in data) { - starter[entry.x.round()] = barGenerator(entry.x.round(), y: entry.value.toDouble(), color: getColor(entry)); + final starter = List.generate(barCount, barGenerator); + for (final entry in data) { + starter[entry.x.round()] = barGenerator( + entry.x.round(), + y: entry.value.toDouble(), + color: getColor(entry), + ); } return starter; } @@ -164,12 +188,13 @@ class AverageSectionWidget extends ReportSectionWidget { final lineCount = barGroups.length * 2; bool drawLine(double val) { // draw when we are at the border between two phases - return (val * lineCount % (2 * subject.study.schedule.phaseDuration)).toInt() == 0; + return (val * lineCount % (2 * subject.study.schedule.phaseDuration)) + .toInt() == + 0; } return FlGridData( drawHorizontalLine: false, - drawVerticalLine: true, checkToShowVerticalLine: drawLine, verticalInterval: 1 / lineCount, ); @@ -183,23 +208,31 @@ class AverageSectionWidget extends ReportSectionWidget { switch (section.aggregate) { case TemporalAggregation.day: //c = colors[subject.interventionOrder.indexOf(diagram.intervention)]; - if (subject.study.schedule.includeBaseline && diagram.x < subject.study.schedule.phaseDuration) { + if (subject.study.schedule.includeBaseline && + diagram.x < subject.study.schedule.phaseDuration) { // if id == "_baseline" c = baselineColor; } else { - c = colors[subject.selectedInterventions.map((e) => e.id).toList().indexOf(diagram.intervention)]; + c = colors[subject.selectedInterventions + .map((e) => e.id) + .toList() + .indexOf(diagram.intervention)]; } case TemporalAggregation.phase: if (subject.study.schedule.includeBaseline && diagram.x == 0) { c = baselineColor; } else { - c = colors[subject.selectedInterventions.map((e) => e.id).toList().indexOf(diagram.intervention)]; + c = colors[subject.selectedInterventions + .map((e) => e.id) + .toList() + .indexOf(diagram.intervention)]; } case TemporalAggregation.intervention: if (subject.study.schedule.includeBaseline && diagram.x == 0) { c = baselineColor; } else { - c = colors[diagram.x.round() - (subject.study.schedule.includeBaseline ? 1 : 0)]; + c = colors[diagram.x.round() - + (subject.study.schedule.includeBaseline ? 1 : 0)]; } default: } @@ -207,7 +240,9 @@ class AverageSectionWidget extends ReportSectionWidget { } int getDayIndex(DateTime key) { - if (subject.study.schedule.includeBaseline) return subject.getDayOfStudyFor(key); + if (subject.study.schedule.includeBaseline) { + return subject.getDayOfStudyFor(key); + } final schedule = subject.scheduleFor(subject.startedAt!); // this always has to be found because studies have to have at least 2 // interventions @@ -256,7 +291,7 @@ class AverageSectionWidget extends ReportSectionWidget { .groupBy((e) => e.intervention) .aggregateWithKey( (data, intervention) => DiagramDatum( - order[intervention] as num, + order[intervention]! as num, foldAggregateMean()(data.map((e) => e.value)), null, intervention, @@ -267,7 +302,10 @@ class AverageSectionWidget extends ReportSectionWidget { } Map getInterventionNames(BuildContext context) { - final names = {for (var intervention in subject.study.interventions) intervention.id: intervention.name}; + final names = { + for (final intervention in subject.study.interventions) + intervention.id: intervention.name, + }; names[Study.baselineID] = AppLocalizations.of(context)!.baseline; return names; } diff --git a/app/lib/screens/study/report/util/plot_utilities.dart b/app/lib/screens/study/report/util/plot_utilities.dart index eddb3cad0..34294da77 100644 --- a/app/lib/screens/study/report/util/plot_utilities.dart +++ b/app/lib/screens/study/report/util/plot_utilities.dart @@ -3,7 +3,7 @@ import 'package:studyu_core/core.dart'; Map getInterventionPositions(List interventions) { final order = {}; - for (var intervention in interventions) { + for (final intervention in interventions) { if (!order.containsKey(intervention.id)) { order[intervention.id] = order.length; } diff --git a/app/lib/screens/study/tasks/intervention/checkmark_task_widget.dart b/app/lib/screens/study/tasks/intervention/checkmark_task_widget.dart index 747cd1430..a363ca969 100644 --- a/app/lib/screens/study/tasks/intervention/checkmark_task_widget.dart +++ b/app/lib/screens/study/tasks/intervention/checkmark_task_widget.dart @@ -18,27 +18,38 @@ class CheckmarkTaskWidget extends StatefulWidget { } class _CheckmarkTaskWidgetState extends State { - DateTime? loginClickTime; + DateTime _lastClickTime = DateTime.now(); bool _isLoading = false; @override Widget build(BuildContext context) { return ElevatedButton.icon( style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(Colors.green), - textStyle: MaterialStateProperty.all(const TextStyle(color: Colors.white))), + backgroundColor: WidgetStateProperty.all(Colors.green), + textStyle: WidgetStateProperty.all( + const TextStyle(color: Colors.white), + ), + ), onPressed: () async { - if (isRedundantClick(loginClickTime)) return; + if (isRedundantClick(_lastClickTime)) return; setState(() { _isLoading = true; + _lastClickTime = DateTime.now(); }); await handleTaskCompletion(context, (StudySubject? subject) async { try { - await subject! - .addResult(taskId: widget.task!.id, periodId: widget.completionPeriod!.id, result: true); + await subject!.addResult( + taskId: widget.task!.id, + periodId: widget.completionPeriod!.id, + result: true, + ); } on SocketException catch (_) { await subject!.addResult( - taskId: widget.task!.id, periodId: widget.completionPeriod!.id, result: true, offline: true); + taskId: widget.task!.id, + periodId: widget.completionPeriod!.id, + result: true, + offline: true, + ); rethrow; } }); @@ -48,7 +59,9 @@ class _CheckmarkTaskWidgetState extends State { if (!context.mounted) return; Navigator.pop(context, true); }, - icon: _isLoading ? const CircularProgressIndicator(color: Colors.white) : const Icon(Icons.check), + icon: _isLoading + ? const CircularProgressIndicator(color: Colors.white) + : const Icon(Icons.check), label: Text(AppLocalizations.of(context)!.complete), ); } diff --git a/app/lib/screens/study/tasks/observation/questionnaire_task_widget.dart b/app/lib/screens/study/tasks/observation/questionnaire_task_widget.dart index e53427ce6..fe551f9d5 100644 --- a/app/lib/screens/study/tasks/observation/questionnaire_task_widget.dart +++ b/app/lib/screens/study/tasks/observation/questionnaire_task_widget.dart @@ -6,33 +6,49 @@ import 'package:studyu_app/screens/study/tasks/task_screen.dart'; import 'package:studyu_app/util/misc.dart'; import 'package:studyu_app/util/study_subject_extension.dart'; import 'package:studyu_app/util/temporary_storage_handler.dart'; -import 'package:studyu_core/core.dart'; import 'package:studyu_app/widgets/questionnaire/questionnaire_widget.dart'; +import 'package:studyu_core/core.dart'; class QuestionnaireTaskWidget extends StatefulWidget { final QuestionnaireTask task; final CompletionPeriod completionPeriod; - const QuestionnaireTaskWidget({required this.task, required this.completionPeriod, super.key}); + const QuestionnaireTaskWidget({ + required this.task, + required this.completionPeriod, + super.key, + }); @override - State createState() => _QuestionnaireTaskWidgetState(); + State createState() => + _QuestionnaireTaskWidgetState(); } class _QuestionnaireTaskWidgetState extends State { dynamic response; late bool responseValidator; - DateTime? loginClickTime; + DateTime _lastClickTime = DateTime.now(); bool _isLoading = false; final GlobalKey formKey = GlobalKey(); - Future _addQuestionnaireResult(T response, BuildContext context) async { + Future _addQuestionnaireResult( + T response, + BuildContext context, + ) async { await handleTaskCompletion(context, (StudySubject? subject) async { try { - await subject!.addResult(taskId: widget.task.id, periodId: widget.completionPeriod.id, result: response); + await subject!.addResult( + taskId: widget.task.id, + periodId: widget.completionPeriod.id, + result: response, + ); } on SocketException catch (_) { await subject!.addResult( - taskId: widget.task.id, periodId: widget.completionPeriod.id, result: response, offline: true); + taskId: widget.task.id, + periodId: widget.completionPeriod.id, + result: response, + offline: true, + ); rethrow; } }); @@ -64,32 +80,40 @@ class _QuestionnaireTaskWidgetState extends State { ), ), ), - response != null && responseValidator - ? ElevatedButton.icon( - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.green)), - onPressed: () async { - if (isRedundantClick(loginClickTime)) { - return; - } - if (!formKey.currentState!.validate()) { - return; - } - setState(() { - _isLoading = true; - }); - switch (response) { - case QuestionnaireState questionnaireState: - await _addQuestionnaireResult(questionnaireState, context); - break; - } - setState(() { - _isLoading = false; - }); - }, - icon: _isLoading ? const CircularProgressIndicator(color: Colors.white) : const Icon(Icons.check), - label: Text(AppLocalizations.of(context)!.complete), - ) - : const SizedBox.shrink(), + if (response != null && responseValidator) + ElevatedButton.icon( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(Colors.green), + ), + onPressed: () async { + if (isRedundantClick(_lastClickTime)) { + return; + } + if (!formKey.currentState!.validate()) { + return; + } + setState(() { + _isLoading = true; + _lastClickTime = DateTime.now(); + }); + switch (response) { + case final QuestionnaireState questionnaireState: + await _addQuestionnaireResult( + questionnaireState, + context, + ); + } + setState(() { + _isLoading = false; + }); + }, + icon: _isLoading + ? const CircularProgressIndicator(color: Colors.white) + : const Icon(Icons.check), + label: Text(AppLocalizations.of(context)!.complete), + ) + else + const SizedBox.shrink(), ], ); } diff --git a/app/lib/screens/study/tasks/task_screen.dart b/app/lib/screens/study/tasks/task_screen.dart index 04a66b34d..53301b424 100644 --- a/app/lib/screens/study/tasks/task_screen.dart +++ b/app/lib/screens/study/tasks/task_screen.dart @@ -1,19 +1,22 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import 'package:studyu_app/models/app_state.dart'; +import 'package:studyu_app/screens/study/tasks/intervention/checkmark_task_widget.dart'; +import 'package:studyu_app/screens/study/tasks/observation/questionnaire_task_widget.dart'; import 'package:studyu_app/util/cache.dart'; import 'package:studyu_app/widgets/html_text.dart'; import 'package:studyu_core/core.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'intervention/checkmark_task_widget.dart'; -import 'observation/questionnaire_task_widget.dart'; class TaskScreen extends StatefulWidget { final TaskInstance taskInstance; - static MaterialPageRoute routeFor({required TaskInstance taskInstance}) => MaterialPageRoute( + static MaterialPageRoute routeFor({ + required TaskInstance taskInstance, + }) => + MaterialPageRoute( builder: (_) => TaskScreen(taskInstance: taskInstance), ); @@ -31,17 +34,19 @@ class _TaskScreenState extends State { void didChangeDependencies() { super.didChangeDependencies(); subject = context.watch().activeSubject; - taskInstance = TaskInstance.fromInstanceId(widget.taskInstance.id, study: subject!.study); + taskInstance = TaskInstance.fromInstanceId( + widget.taskInstance.id, + study: subject!.study, + ); } Widget _buildTask() { switch (taskInstance.task) { - case CheckmarkTask checkmarkTask: + case final CheckmarkTask checkmarkTask: return SingleChildScrollView( child: SizedBox( width: MediaQuery.of(context).size.width, child: Column( - crossAxisAlignment: CrossAxisAlignment.center, children: [ HtmlText(taskInstance.task.header, centered: true), const SizedBox(height: 20), @@ -49,12 +54,12 @@ class _TaskScreenState extends State { task: checkmarkTask, key: UniqueKey(), completionPeriod: taskInstance.completionPeriod, - ) + ), ], ), ), ); - case QuestionnaireTask questionnaireTask: + case final QuestionnaireTask questionnaireTask: return QuestionnaireTaskWidget( task: questionnaireTask, key: UniqueKey(), @@ -77,7 +82,10 @@ class _TaskScreenState extends State { } } -handleTaskCompletion(BuildContext context, Function(StudySubject?) completionCallback) async { +Future handleTaskCompletion( + BuildContext context, + Function(StudySubject?) completionCallback, +) async { final state = context.read(); final activeSubject = state.activeSubject; try { @@ -93,7 +101,10 @@ handleTaskCompletion(BuildContext context, Function(StudySubject?) completionCal SnackBar( content: Text(AppLocalizations.of(context)!.could_not_save_results), duration: const Duration(seconds: 10), - action: SnackBarAction(label: 'Retry', onPressed: () => handleTaskCompletion(context, completionCallback)), + action: SnackBarAction( + label: 'Retry', + onPressed: () => handleTaskCompletion(context, completionCallback), + ), ), ); rethrow; diff --git a/app/lib/theme.dart b/app/lib/theme.dart index 37fc09fa3..b1b7a8859 100644 --- a/app/lib/theme.dart +++ b/app/lib/theme.dart @@ -19,7 +19,7 @@ ThemeData get theme => ThemeData( ), elevatedButtonTheme: ElevatedButtonThemeData( style: ButtonStyle( - foregroundColor: MaterialStateProperty.all(Colors.white), + foregroundColor: WidgetStateProperty.all(Colors.white), ), ), visualDensity: VisualDensity.adaptivePlatformDensity, diff --git a/app/lib/util/app_analytics.dart b/app/lib/util/app_analytics.dart index 2d11c4481..32871a838 100644 --- a/app/lib/util/app_analytics.dart +++ b/app/lib/util/app_analytics.dart @@ -4,11 +4,10 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_logging/sentry_logging.dart'; import 'package:studyu_app/app.dart'; import 'package:studyu_app/models/app_state.dart'; +import 'package:studyu_app/util/cache.dart'; import 'package:studyu_core/core.dart'; import 'package:studyu_flutter_common/studyu_flutter_common.dart'; -import 'cache.dart'; - class AppAnalytics /*extends Analytics*/ { static bool? _userEnabled; @@ -35,8 +34,9 @@ class AppAnalytics /*extends Analytics*/ { static Future start(AppConfig? appConfig, MyApp myApp) async { StudyUAnalytics? studyUAnalytics; - if (appConfig == null || appConfig.analytics != null && appConfig.analytics!.dsn.isEmpty) { - final cachedAnalytics = (await Cache.loadAnalytics()); + if (appConfig == null || + appConfig.analytics != null && appConfig.analytics!.dsn.isEmpty) { + final cachedAnalytics = await Cache.loadAnalytics(); if (cachedAnalytics != null) { studyUAnalytics = cachedAnalytics; } @@ -48,25 +48,30 @@ class AppAnalytics /*extends Analytics*/ { runApp(myApp); return; } - await SentryFlutter.init((options) { - options.dsn = studyUAnalytics!.dsn; - // Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring. - // We recommend adjusting this value in production. - options.tracesSampleRate = studyUAnalytics.samplingRate ?? 1.0; - options.addIntegration(LoggingIntegration()); - }, appRunner: () => runApp(myApp)); - Cache.storeAnalytics(StudyUAnalytics( - studyUAnalytics.enabled, - studyUAnalytics.dsn, - studyUAnalytics.samplingRate, - )); + await SentryFlutter.init( + (options) { + options.dsn = studyUAnalytics!.dsn; + // Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring. + // We recommend adjusting this value in production. + options.tracesSampleRate = studyUAnalytics.samplingRate ?? 1.0; + options.addIntegration(LoggingIntegration()); + }, + appRunner: () => runApp(myApp), + ); + Cache.storeAnalytics( + StudyUAnalytics( + studyUAnalytics.enabled, + studyUAnalytics.dsn, + studyUAnalytics.samplingRate, + ), + ); } - static get isUserEnabled { + static bool? get isUserEnabled { return _userEnabled; } - static void setEnabled(bool newEnabled) async { + static Future setEnabled(bool newEnabled) async { await SecureStorage.write(keyAnalyticsUserEnable, newEnabled.toString()); if (!newEnabled) { // a restart of the app will be necessary to enable sentry again @@ -90,9 +95,11 @@ class AppAnalytics /*extends Analytics*/ { void initAdvanced() { Sentry.configureScope((scope) { - scope.setUser(SentryUser( - id: subject!.userId, - )); + scope.setUser( + SentryUser( + id: subject!.userId, + ), + ); final advancedContext = { 'subjectId': subject!.id, 'studyId': state.selectedStudy!.id, diff --git a/app/lib/util/cache.dart b/app/lib/util/cache.dart index 751d6c9ab..4747459b6 100644 --- a/app/lib/util/cache.dart +++ b/app/lib/util/cache.dart @@ -19,19 +19,29 @@ class Cache { static Future loadSubject() async { // debugPrint("Load subject from cache"); if (await SecureStorage.containsKey(cacheSubjectKey)) { - return StudySubject.fromJson(jsonDecode((await SecureStorage.read(cacheSubjectKey))!)); + return StudySubject.fromJson( + jsonDecode((await SecureStorage.read(cacheSubjectKey))!) + as Map, + ); } else { throw Exception("No cached subject found"); } } static Future storeAnalytics(StudyUAnalytics analytics) async { - SecureStorage.write(StudyUAnalytics.keyStudyUAnalytics, jsonEncode(analytics.toJson())); + SecureStorage.write( + StudyUAnalytics.keyStudyUAnalytics, + jsonEncode(analytics.toJson()), + ); } static Future loadAnalytics() async { if (await SecureStorage.containsKey(cacheSubjectKey)) { - return StudyUAnalytics.fromJson(jsonDecode((await SecureStorage.read(StudyUAnalytics.keyStudyUAnalytics))!)); + return StudyUAnalytics.fromJson( + jsonDecode( + (await SecureStorage.read(StudyUAnalytics.keyStudyUAnalytics))!, + ) as Map, + ); } return null; } @@ -45,7 +55,10 @@ class Cache { final blobStorageHandler = BlobStorageHandler(); final futureBlobFiles = await TemporaryStorageHandler.getFutureBlobFiles(); for (final futureBlobFile in futureBlobFiles) { - await blobStorageHandler.uploadObservation(futureBlobFile.futureBlobId, File(futureBlobFile.localFilePath)); + await blobStorageHandler.uploadObservation( + futureBlobFile.futureBlobId, + File(futureBlobFile.localFilePath), + ); await File(futureBlobFile.localFilePath).delete(); } } @@ -53,12 +66,17 @@ class Cache { static Future synchronize(StudySubject remoteSubject) async { if (isSynchronizing) return remoteSubject; // No local subject found - if (!(await SecureStorage.containsKey(cacheSubjectKey))) return remoteSubject; + if (!(await SecureStorage.containsKey(cacheSubjectKey))) { + return remoteSubject; + } final localSubject = await loadSubject(); // local and remote subject are equal, nothing to synchronize if (localSubject == remoteSubject) return remoteSubject; // remote subject belongs to a different study - if (!kDebugMode && remoteSubject.startedAt!.isAfter(localSubject.startedAt!)) return remoteSubject; + if (!kDebugMode && + remoteSubject.startedAt!.isAfter(localSubject.startedAt!)) { + return remoteSubject; + } debugPrint("Synchronize subject with cache"); isSynchronizing = true; @@ -82,17 +100,27 @@ class Cache { newProgress = localSubject.progress; }*/ // save new progress - final List newProgress = [...localSubject.progress, ...remoteSubject.progress]; + final List newProgress = [ + ...localSubject.progress, + ...remoteSubject.progress, + ]; newProgress.removeWhere( - (element) => localSubject.progress.contains(element) && remoteSubject.progress.contains(element)); - for (var p in newProgress) { + (element) => + localSubject.progress.contains(element) && + remoteSubject.progress.contains(element), + ); + for (final p in newProgress) { await p.save(); } // merge local and remote progress and remove duplicates - final List finalProgress = [...localSubject.progress, ...remoteSubject.progress]; + final List finalProgress = [ + ...localSubject.progress, + ...remoteSubject.progress, + ]; final duplicates = {}; - finalProgress.retainWhere((element) => duplicates.add(element.completedAt)); + finalProgress + .retainWhere((element) => duplicates.add(element.completedAt)); // replace remote progress with our merge remoteSubject.progress = finalProgress; await remoteSubject.save(); @@ -101,10 +129,15 @@ class Cache { // We can either drop local or overwrite remote // ... for now do nothing if (!kDebugMode && localSubject.startedAt == remoteSubject.startedAt) { - StudyULogger.fatal("Cache synchronization found local changes that cannot be merged"); + StudyULogger.fatal( + "Cache synchronization found local changes that cannot be merged", + ); StudyUDiagnostics.captureMessage( - "localSubject: ${localSubject.toFullJson()} \nremoteSubject: ${remoteSubject.toFullJson()}"); - StudyUDiagnostics.captureException(Exception("CacheSynchronizationException")); + "localSubject: ${localSubject.toFullJson()} \nremoteSubject: ${remoteSubject.toFullJson()}", + ); + StudyUDiagnostics.captureException( + Exception("CacheSynchronizationException"), + ); } } } catch (exception) { diff --git a/app/lib/util/color.dart b/app/lib/util/color.dart index c5fce8538..b83855db1 100644 --- a/app/lib/util/color.dart +++ b/app/lib/util/color.dart @@ -5,7 +5,8 @@ extension ColorBrightness on Color { assert(amount >= 0 && amount <= 1); final hsl = HSLColor.fromColor(this); - final hslLight = hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0).toDouble()); + final hslLight = + hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0)); return hslLight.toColor(); } diff --git a/app/lib/util/data_processing.dart b/app/lib/util/data_processing.dart index 82495e134..16487ddb5 100644 --- a/app/lib/util/data_processing.dart +++ b/app/lib/util/data_processing.dart @@ -15,7 +15,9 @@ class GroupedIterable extends Iterable>> { Iterable> aggregate(FoldAggregator aggregator) => map((entry) => MapEntry(entry.key, aggregator(entry.value))); - Iterable> aggregateWithKey(KeyedAggregator aggregator) => + Iterable> aggregateWithKey( + KeyedAggregator aggregator, + ) => map((entry) => MapEntry(entry.key, aggregator(entry.value, entry.key))); } @@ -30,14 +32,18 @@ FoldAggregator foldAggregateMedian() => (values) { FoldAggregator foldAggregateMax() => (values) => values.reduce((a, b) => a.compareTo(b) > 0 ? a : b); -FoldAggregator foldAggregateSum() => (values) => values.reduce((value, element) => value + element); +FoldAggregator foldAggregateSum() => + (values) => values.reduce((value, element) => value + element); -FoldAggregator foldAggregateMean() => (values) => foldAggregateSum()(values) / values.length; +FoldAggregator foldAggregateMean() => + (values) => foldAggregateSum()(values) / values.length; extension GroupByIterable on Iterable { GroupedIterable groupBy(KeyAccessor key) { final result = >{}; - forEach((element) => result.putIfAbsent(key(element), () => []).add(element)); + forEach( + (element) => result.putIfAbsent(key(element), () => []).add(element), + ); return GroupedIterable.from(result); } } diff --git a/app/lib/util/intervention.dart b/app/lib/util/intervention.dart index de63c04f3..17f064344 100644 --- a/app/lib/util/intervention.dart +++ b/app/lib/util/intervention.dart @@ -3,12 +3,20 @@ import 'package:material_design_icons_flutter/material_design_icons_flutter.dart import 'package:studyu_core/core.dart'; Widget interventionIcon(Intervention intervention, {Color? color}) { - if (intervention.isBaseline()) return Icon(MdiIcons.rayStart, color: color ?? Colors.white); + if (intervention.isBaseline()) { + return Icon(MdiIcons.rayStart, color: color ?? Colors.white); + } return intervention.icon.isNotEmpty - ? Icon(MdiIcons.fromString(intervention.icon), color: color ?? Colors.white) + ? Icon( + MdiIcons.fromString(intervention.icon), + color: color ?? Colors.white, + ) : Text( intervention.name![0].toUpperCase(), - style: TextStyle(fontWeight: FontWeight.bold, color: color ?? Colors.white), + style: TextStyle( + fontWeight: FontWeight.bold, + color: color ?? Colors.white, + ), ); } diff --git a/app/lib/util/misc.dart b/app/lib/util/misc.dart index c66346d85..e0ab7bce5 100644 --- a/app/lib/util/misc.dart +++ b/app/lib/util/misc.dart @@ -1,16 +1,10 @@ -bool isRedundantClick(DateTime? loginClickTime) { - final currentTime = DateTime.now(); - if (loginClickTime == null) { - loginClickTime = currentTime; +bool isRedundantClick( + DateTime lastClickTime, { + Duration interval = const Duration(seconds: 2), +}) { + final now = DateTime.now(); + if (now.difference(lastClickTime) > interval) { return false; } - int secondsUntilClicked = currentTime.difference(loginClickTime).inSeconds; - // timeout submit button to disable multiple clicks - if (secondsUntilClicked < 3) { - print('complete button is still frozen'); - return true; - } - - loginClickTime = currentTime; - return false; + return true; } diff --git a/app/lib/util/notifications.dart b/app/lib/util/notifications.dart index ca38fd6c8..92716fcd7 100644 --- a/app/lib/util/notifications.dart +++ b/app/lib/util/notifications.dart @@ -6,13 +6,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:studyu_app/main.dart'; import 'package:studyu_app/routes.dart'; import 'package:studyu_app/screens/study/dashboard/dashboard.dart'; +import 'package:studyu_app/screens/study/tasks/task_screen.dart'; import 'package:studyu_core/core.dart'; -import '../main.dart'; -import '../screens/study/tasks/task_screen.dart'; - class NotificationValidators { bool didNotificationLaunchApp = false; // do not launch notification action twice if user subscribes to a new study @@ -20,19 +19,25 @@ class NotificationValidators { bool wasNotificationActionCompleted = false; NotificationValidators( - this.didNotificationLaunchApp, this.wasNotificationActionHandled, this.wasNotificationActionCompleted); + this.didNotificationLaunchApp, + this.wasNotificationActionHandled, + this.wasNotificationActionCompleted, + ); } class StudyNotifications { StudySubject? subject; late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; BuildContext context; - final StreamController didReceiveLocalNotificationStream = + final StreamController + didReceiveLocalNotificationStream = StreamController.broadcast(); - final StreamController selectNotificationStream = StreamController.broadcast(); + final StreamController selectNotificationStream = + StreamController.broadcast(); // String? _taskCannotBeCompleted; - static final NotificationValidators validator = NotificationValidators(false, false, false); + static final NotificationValidators validator = + NotificationValidators(false, false, false); static const bool debug = false; //kDebugMode; static String? scheduledNotificationsDebug; @@ -53,15 +58,18 @@ class StudyNotifications { BuildContext context, ) async { final notifications = StudyNotifications._create(activeSubject, context); - final NotificationAppLaunchDetails? notificationAppLaunchDetails = !kIsWeb && Platform.isLinux - ? null - : await notifications.flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); + final NotificationAppLaunchDetails? notificationAppLaunchDetails = + !kIsWeb && Platform.isLinux + ? null + : await notifications.flutterLocalNotificationsPlugin + .getNotificationAppLaunchDetails(); StudyNotifications.validator.didNotificationLaunchApp = notificationAppLaunchDetails?.didNotificationLaunchApp ?? false; if (StudyNotifications.validator.didNotificationLaunchApp && !StudyNotifications.validator.wasNotificationActionHandled) { StudyNotifications.validator.wasNotificationActionHandled = true; - final selectedNotificationPayload = notificationAppLaunchDetails!.notificationResponse!.payload!; + final selectedNotificationPayload = + notificationAppLaunchDetails!.notificationResponse!.payload!; notifications.handleNotificationResponse(selectedNotificationPayload); } return notifications; @@ -71,7 +79,8 @@ class StudyNotifications { if (Platform.isAndroid) { //final bool granted = await flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation()! + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>()! .areNotificationsEnabled(); } } @@ -79,14 +88,16 @@ class StudyNotifications { Future _requestPermissions() async { if (Platform.isIOS || Platform.isMacOS) { await flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation() + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin>() ?.requestPermissions( alert: true, badge: true, sound: true, ); await flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation() + .resolvePlatformSpecificImplementation< + MacOSFlutterLocalNotificationsPlugin>() ?.requestPermissions( alert: true, badge: true, @@ -94,12 +105,14 @@ class StudyNotifications { ); } else if (Platform.isAndroid) { // todo look into this further if notifications are not received on Android - final AndroidFlutterLocalNotificationsPlugin? androidImplementation = flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation(); + final AndroidFlutterLocalNotificationsPlugin? androidImplementation = + flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>(); - /*final bool granted =*/ await androidImplementation?.requestNotificationsPermission(); + /*final bool granted =*/ await androidImplementation + ?.requestNotificationsPermission(); - var status = await Permission.ignoreBatteryOptimizations.status; + final status = await Permission.ignoreBatteryOptimizations.status; if (status.isDenied) { if (await Permission.ignoreBatteryOptimizations.request().isGranted) { // print("Ignore battery optimization Permission is granted"); @@ -111,12 +124,17 @@ class StudyNotifications { } void _configureDidReceiveLocalNotificationSubject() { - didReceiveLocalNotificationStream.stream.listen((ReceivedNotification receivedNotification) async { + didReceiveLocalNotificationStream.stream + .listen((ReceivedNotification receivedNotification) async { await showDialog( context: context, builder: (BuildContext context) => CupertinoAlertDialog( - title: receivedNotification.title != null ? Text(receivedNotification.title!) : null, - content: receivedNotification.body != null ? Text(receivedNotification.body!) : null, + title: receivedNotification.title != null + ? Text(receivedNotification.title!) + : null, + content: receivedNotification.body != null + ? Text(receivedNotification.body!) + : null, actions: [ CupertinoDialogAction( isDefaultAction: true, @@ -129,7 +147,7 @@ class StudyNotifications { ); }, child: const Text('Ok'), - ) + ), ], ), ); @@ -146,7 +164,8 @@ class StudyNotifications { flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@drawable/ic_notification'); - final DarwinInitializationSettings initializationSettingsDarwin = DarwinInitializationSettings( + final DarwinInitializationSettings initializationSettingsDarwin = + DarwinInitializationSettings( onDidReceiveLocalNotification: ( int id, String? title, @@ -163,10 +182,12 @@ class StudyNotifications { ); }, ); - const LinuxInitializationSettings initializationSettingsLinux = LinuxInitializationSettings( + const LinuxInitializationSettings initializationSettingsLinux = + LinuxInitializationSettings( defaultActionName: 'Open notification', ); - final InitializationSettings initializationSettings = InitializationSettings( + final InitializationSettings initializationSettings = + InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsDarwin, macOS: initializationSettingsDarwin, @@ -174,11 +195,11 @@ class StudyNotifications { ); flutterLocalNotificationsPlugin.initialize( initializationSettings, - onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) { + onDidReceiveNotificationResponse: + (NotificationResponse notificationResponse) { switch (notificationResponse.notificationResponseType) { case NotificationResponseType.selectedNotification: selectNotificationStream.add(notificationResponse.payload); - break; case NotificationResponseType.selectedNotificationAction: /*if (notificationResponse.actionId == navigationActionId) { selectNotificationStream.add(notificationResponse.payload); @@ -192,7 +213,8 @@ class StudyNotifications { Future handleNotificationResponse(String taskInstanceId) async { final nowDt = DateTime.now(); - final taskToRun = TaskInstance.fromInstanceId(taskInstanceId, subject: subject); + final taskToRun = + TaskInstance.fromInstanceId(taskInstanceId, subject: subject); final completed = subject!.completedTaskInstanceForDay( taskToRun.task.id, @@ -201,14 +223,16 @@ class StudyNotifications { ); //if (taskToRun != null) { - final isInsidePeriod = taskToRun.completionPeriod.contains(StudyUTimeOfDay.now()); + final isInsidePeriod = + taskToRun.completionPeriod.contains(StudyUTimeOfDay.now()); if (!completed && isInsidePeriod) { await navigatorKey.currentState!.push( MaterialPageRoute( builder: (_) => TaskScreen(taskInstance: taskToRun), ), ); - navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.loading, (_) => false); + navigatorKey.currentState! + .pushNamedAndRemoveUntil(Routes.loading, (_) => false); // todo error management after null safety /*} else { navigatorKey.currentState!.push( @@ -222,7 +246,8 @@ class StudyNotifications { navigatorKey.currentState!.push( // todo translate MaterialPageRoute( - builder: (_) => const DashboardScreen(error: 'Task could not be found'), + builder: (_) => + const DashboardScreen(error: 'Task could not be found'), ), ); } diff --git a/app/lib/util/save_pdf.dart b/app/lib/util/save_pdf.dart index d04bba411..ebdc3d861 100644 --- a/app/lib/util/save_pdf.dart +++ b/app/lib/util/save_pdf.dart @@ -4,9 +4,15 @@ import 'package:flutter_file_dialog/flutter_file_dialog.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; -Future savePDF(BuildContext context, String title, List content) async { +Future savePDF( + BuildContext context, + String title, + List content, +) async { final doc = pw.Document(); - final logo = pw.MemoryImage((await rootBundle.load('assets/icon/logo.png')).buffer.asUint8List()); + final logo = pw.MemoryImage( + (await rootBundle.load('assets/icon/logo.png')).buffer.asUint8List(), + ); doc.addPage( pw.MultiPage( pageFormat: PdfPageFormat.a4, diff --git a/app/lib/util/schedule_notifications.dart b/app/lib/util/schedule_notifications.dart index ac9942724..a87613953 100644 --- a/app/lib/util/schedule_notifications.dart +++ b/app/lib/util/schedule_notifications.dart @@ -3,23 +3,36 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:provider/provider.dart'; +import 'package:studyu_app/models/app_state.dart'; +import 'package:studyu_app/util/notifications.dart'; import 'package:studyu_core/core.dart'; import 'package:timezone/timezone.dart' as tz; -import '../models/app_state.dart'; -import 'notifications.dart'; - -Future scheduleReminderForDate(FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin, int id, - String body, StudyNotification studyNotification, NotificationDetails notificationDetails) async { +Future scheduleReminderForDate( + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin, + int id, + String body, + StudyNotification studyNotification, + NotificationDetails notificationDetails, +) async { var currentId = id; final task = studyNotification.taskInstance.task; final date = studyNotification.date; for (final reminder in task.schedule.reminders) { // unlock time: ${task.schedule.completionPeriods.firstWhere((cp) => cp.unlockTime.earlierThan(reminder)).lockTime} - final reminderTime = tz.TZDateTime(tz.local, date.year, date.month, date.day, reminder.hour, reminder.minute); + final reminderTime = tz.TZDateTime( + tz.local, + date.year, + date.month, + date.day, + reminder.hour, + reminder.minute, + ); if (date.isSameDate(DateTime.now()) && - !StudyUTimeOfDay(hour: date.hour, minute: date.minute).earlierThan(reminder, exact: true)) { - String debugStr = 'Skipped #$currentId: $reminderTime, ${task.title}, ${studyNotification.taskInstance.id}'; + !StudyUTimeOfDay(hour: date.hour, minute: date.minute) + .earlierThan(reminder, exact: true)) { + final String debugStr = + 'Skipped #$currentId: $reminderTime, ${task.title}, ${studyNotification.taskInstance.id}'; //StudyNotifications.scheduledNotificationsDebug += '\n\n$debugStr'; if (StudyNotifications.debug) { print(debugStr); @@ -43,7 +56,8 @@ Future scheduleReminderForDate(FlutterLocalNotificationsPlugin flutterLocal reminderTime, notificationDetails, payload: studyNotification.taskInstance.id, - uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.wallClockTime, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.wallClockTime, // exactAllowWhileIdle only works if the exact alarm permission has been granted androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle, ); @@ -60,8 +74,10 @@ Future scheduleReminderForDate(FlutterLocalNotificationsPlugin flutterLocal ); }*/ // DEBUG: List scheduled notifications - String debugStr = 'Scheduled #$currentId: $reminderTime, ${task.title}, ${studyNotification.taskInstance.id}'; - StudyNotifications.scheduledNotificationsDebug = '${StudyNotifications.scheduledNotificationsDebug}\n\n$debugStr'; + final String debugStr = + 'Scheduled #$currentId: $reminderTime, ${task.title}, ${studyNotification.taskInstance.id}'; + StudyNotifications.scheduledNotificationsDebug = + '${StudyNotifications.scheduledNotificationsDebug}\n\n$debugStr'; if (StudyNotifications.debug) { print(debugStr); } @@ -70,7 +86,8 @@ Future scheduleReminderForDate(FlutterLocalNotificationsPlugin flutterLocal return currentId; } -const notificationDetails = NotificationDetails(android: AndroidNotificationDetails('0', 'StudyU')); +const notificationDetails = + NotificationDetails(android: AndroidNotificationDetails('0', 'StudyU')); Future scheduleNotifications(BuildContext context) async { if (StudyNotifications.debug) { @@ -85,13 +102,15 @@ Future scheduleNotifications(BuildContext context) async { final appState = context.read(); final subject = appState.activeSubject!; final body = AppLocalizations.of(context)!.study_notification_body; - final studyNotifications = appState.studyNotifications ?? await StudyNotifications.create(subject, context); + final studyNotifications = appState.studyNotifications ?? + await StudyNotifications.create(subject, context); - final notificationsPlugin = studyNotifications.flutterLocalNotificationsPlugin; + final notificationsPlugin = + studyNotifications.flutterLocalNotificationsPlugin; await notificationsPlugin.cancelAll(); StudyNotifications.scheduledNotificationsDebug = - "Timestamp: ${DateTime.now().toString()}\nSubject ID: ${subject.id}\n"; + "Timestamp: ${DateTime.now()}\nSubject ID: ${subject.id}\n"; final List studyNotificationList = []; for (int index = 0; index <= 3; index++) { @@ -101,33 +120,50 @@ Future scheduleNotifications(BuildContext context) async { } var id = 0; for (final StudyNotification notification in studyNotificationList) { - final currentId = await scheduleReminderForDate(notificationsPlugin, id, body, notification, notificationDetails); + final currentId = await scheduleReminderForDate( + notificationsPlugin, + id, + body, + notification, + notificationDetails, + ); id = currentId; } } -List _buildNotificationList(StudySubject subject, DateTime date, List tasks) { - List taskNotifications = []; - for (TaskInstance taskInstance in tasks) { +List _buildNotificationList( + StudySubject subject, + DateTime date, + List tasks, +) { + final List taskNotifications = []; + for (final TaskInstance taskInstance in tasks) { if (taskInstance.task.title == null || taskInstance.task.title!.isEmpty) { return []; } - if (!subject.completedTaskInstanceForDay(taskInstance.task.id, taskInstance.completionPeriod, date)) { + if (!subject.completedTaskInstanceForDay( + taskInstance.task.id, + taskInstance.completionPeriod, + date, + )) { taskNotifications.add(StudyNotification(taskInstance, date)); } else { - String debugStr = 'TaskInstance already completed: ${taskInstance.completionPeriod}, ${taskInstance.task.title}'; - StudyNotifications.scheduledNotificationsDebug = '${StudyNotifications.scheduledNotificationsDebug}\n\n$debugStr'; + final String debugStr = + 'TaskInstance already completed: ${taskInstance.completionPeriod}, ${taskInstance.task.title}'; + StudyNotifications.scheduledNotificationsDebug = + '${StudyNotifications.scheduledNotificationsDebug}\n\n$debugStr'; } } return taskNotifications; } -void testNotifications(BuildContext context) async { +Future testNotifications(BuildContext context) async { // Notifications not supported on web if (kIsWeb) return; final appState = context.read(); final subject = appState.activeSubject; - final studyNotifications = appState.studyNotifications ?? await StudyNotifications.create(subject, context); + final studyNotifications = appState.studyNotifications ?? + await StudyNotifications.create(subject, context); await studyNotifications.flutterLocalNotificationsPlugin.show( /*******************/ 99, diff --git a/app/lib/util/study_subject_extension.dart b/app/lib/util/study_subject_extension.dart index a8c12d5fe..edd51c827 100644 --- a/app/lib/util/study_subject_extension.dart +++ b/app/lib/util/study_subject_extension.dart @@ -1,9 +1,8 @@ import 'package:flutter/foundation.dart'; +import 'package:studyu_app/util/cache.dart'; import 'package:studyu_app/util/temporary_storage_handler.dart'; import 'package:studyu_core/core.dart'; -import 'cache.dart'; - extension StudySubjectExtension on StudySubject { Future addResult({ required String taskId, @@ -12,7 +11,11 @@ extension StudySubjectExtension on StudySubject { bool offline = false, }) async { final Result resultObject = switch (result) { - QuestionnaireState() => Result.app(type: 'QuestionnaireState', periodId: periodId, result: result), + QuestionnaireState() => Result.app( + type: 'QuestionnaireState', + periodId: periodId, + result: result, + ), bool() => Result.app(type: 'bool', periodId: periodId, result: result), _ => Result.app(type: 'unknown', periodId: periodId, result: result), }; @@ -31,11 +34,14 @@ extension StudySubjectExtension on StudySubject { if (answer.response is FutureBlobFile) { final futureBlobFile = answer.response as FutureBlobFile; await TemporaryStorageHandler.moveStagingFileToUploadDirectory( - futureBlobFile.localFilePath, futureBlobFile.futureBlobId); + futureBlobFile.localFilePath, + futureBlobFile.futureBlobId, + ); // Replaces Answer with Answer - questionnaireState.answers[answerEntry.key] = Answer(answer.question, answer.timestamp) - ..response = futureBlobFile.futureBlobId; + questionnaireState.answers[answerEntry.key] = + Answer(answer.question, answer.timestamp) + ..response = futureBlobFile.futureBlobId; } } } diff --git a/app/lib/util/temporary_storage_handler.dart b/app/lib/util/temporary_storage_handler.dart index 588293810..b2c35017b 100644 --- a/app/lib/util/temporary_storage_handler.dart +++ b/app/lib/util/temporary_storage_handler.dart @@ -23,26 +23,34 @@ class TemporaryStorageHandler { static Future _getMultimodalTempDirectory() async { final tempAppData = await getTemporaryDirectory(); - final multimodalTempDirectory = Directory("${tempAppData.path}/multimodal-temp"); + final multimodalTempDirectory = + Directory("${tempAppData.path}/multimodal-temp"); await multimodalTempDirectory.create(recursive: true); return multimodalTempDirectory; } static Future _getMultimodalUploadDirectory() async { final appData = await getApplicationDocumentsDirectory(); - final multimodalUploadDirectory = Directory("${appData.path}/multimodal-upload"); + final multimodalUploadDirectory = + Directory("${appData.path}/multimodal-upload"); await multimodalUploadDirectory.create(recursive: true); return multimodalUploadDirectory; } - static Future moveStagingFileToUploadDirectory(String stagingFilePath, String blobId) async { + static Future moveStagingFileToUploadDirectory( + String stagingFilePath, + String blobId, + ) async { final stagingFile = File(stagingFilePath); final uploadDirectory = await _getMultimodalUploadDirectory(); - final uploadFile = File(path.join( + final uploadFile = File( + path.join( uploadDirectory.path, [ blobId, - ].join())); + ].join(), + ), + ); await stagingFile.rename(uploadFile.path); } @@ -67,7 +75,8 @@ class TemporaryStorageHandler { TemporaryStorageHandler._audioFileType, ].join(), ); - final futureBlobId = [fileName, TemporaryStorageHandler._audioFileType].join(); + final futureBlobId = + [fileName, TemporaryStorageHandler._audioFileType].join(); return FutureBlobFile(localFilePath, futureBlobId); } @@ -82,7 +91,8 @@ class TemporaryStorageHandler { TemporaryStorageHandler._imageFileType, ].join(), ); - final futureBlobId = [fileName, TemporaryStorageHandler._imageFileType].join(); + final futureBlobId = + [fileName, TemporaryStorageHandler._imageFileType].join(); return FutureBlobFile(localFilePath, futureBlobId); } @@ -90,7 +100,11 @@ class TemporaryStorageHandler { final temporaryMultimodalDirectory = await _getMultimodalTempDirectory(); for (final file in await temporaryMultimodalDirectory .list() - .where((f) => path.basename(f.path).startsWith(TemporaryStorageHandler._stagingBaseNamePrefix)) + .where( + (f) => path + .basename(f.path) + .startsWith(TemporaryStorageHandler._stagingBaseNamePrefix), + ) .toList()) { await file.delete(); } diff --git a/app/lib/widgets/bottom_onboarding_navigation.dart b/app/lib/widgets/bottom_onboarding_navigation.dart index 423405738..6dfb0d023 100644 --- a/app/lib/widgets/bottom_onboarding_navigation.dart +++ b/app/lib/widgets/bottom_onboarding_navigation.dart @@ -42,7 +42,7 @@ class BottomOnboardingNavigation extends StatelessWidget { if (progress != null) ...[ const SizedBox(width: 8), Expanded(child: progress!), - const SizedBox(width: 8) + const SizedBox(width: 8), ] else const Spacer(), Visibility( diff --git a/app/lib/widgets/html_text.dart b/app/lib/widgets/html_text.dart index e0e455778..dcc961608 100644 --- a/app/lib/widgets/html_text.dart +++ b/app/lib/widgets/html_text.dart @@ -15,14 +15,16 @@ class HtmlText extends StatelessWidget { @override Widget build(BuildContext context) { - Widget htmlWidget = HtmlWidget( + final Widget htmlWidget = HtmlWidget( text ?? '', textStyle: style, // these callbacks are called when a complicated element is loading // or failed to render allowing the app to render progress indicator // and fallback widget - onErrorBuilder: (context, element, error) => Text('$element Error: $error'), - onLoadingBuilder: (context, element, loadingProgress) => const CircularProgressIndicator(), + onErrorBuilder: (context, element, error) => + Text('$element Error: $error'), + onLoadingBuilder: (context, element, loadingProgress) => + const CircularProgressIndicator(), ); return SingleChildScrollView( diff --git a/app/lib/widgets/intervention_card.dart b/app/lib/widgets/intervention_card.dart index b60d16433..326c5cf34 100644 --- a/app/lib/widgets/intervention_card.dart +++ b/app/lib/widgets/intervention_card.dart @@ -35,8 +35,10 @@ class InterventionCard extends StatelessWidget { onTap: onTap, selected: selected, ), - if (showDescription) InterventionCardDescription(intervention: intervention), - if (showTasks && intervention.tasks.isNotEmpty) _TaskList(tasks: intervention.tasks) + if (showDescription) + InterventionCardDescription(intervention: intervention), + if (showTasks && intervention.tasks.isNotEmpty) + _TaskList(tasks: intervention.tasks), ], ); } @@ -63,17 +65,23 @@ class InterventionCardTitle extends StatelessWidget { final theme = Theme.of(context); return ListTile( onTap: onTap, - leading: Icon(MdiIcons.fromString(intervention!.icon), color: theme.colorScheme.secondary), + leading: Icon( + MdiIcons.fromString(intervention!.icon), + color: theme.colorScheme.secondary, + ), trailing: showCheckbox ? Checkbox( value: selected, - onChanged: (_) => onTap!(), // Needed so Checkbox can be clicked and has color + onChanged: (_) => + onTap!(), // Needed so Checkbox can be clicked and has color ) : null, dense: true, title: Row( children: [ - Expanded(child: Text(intervention!.name!, style: theme.textTheme.titleLarge)), + Expanded( + child: Text(intervention!.name!, style: theme.textTheme.titleLarge), + ), if (showDescriptionButton) IconButton( icon: const Icon(Icons.info_outline), @@ -85,9 +93,15 @@ class InterventionCardTitle extends StatelessWidget { : intervention!.description; return AlertDialog( title: ListTile( - leading: Icon(MdiIcons.fromString(intervention!.icon), color: theme.colorScheme.secondary), + leading: Icon( + MdiIcons.fromString(intervention!.icon), + color: theme.colorScheme.secondary, + ), dense: true, - title: Text(intervention!.name!, style: theme.textTheme.titleLarge), + title: Text( + intervention!.name!, + style: theme.textTheme.titleLarge, + ), ), content: HtmlText(description), ); @@ -109,15 +123,17 @@ class InterventionCardDescription extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); - final description = - intervention.isBaseline() ? AppLocalizations.of(context)!.baseline_description : intervention.description; + final description = intervention.isBaseline() + ? AppLocalizations.of(context)!.baseline_description + : intervention.description; if (description == null) return Container(); return Padding( padding: const EdgeInsets.fromLTRB(16, 4, 16, 8), child: Text( description, - style: theme.textTheme.bodyMedium!.copyWith(color: theme.textTheme.bodySmall!.color), + style: theme.textTheme.bodyMedium! + .copyWith(color: theme.textTheme.bodySmall!.color), ), ); } @@ -130,7 +146,10 @@ class _TaskList extends StatelessWidget { String scheduleString(List schedules) { return schedules - .map((completionPeriod) => '${completionPeriod.unlockTime} - ${completionPeriod.lockTime}') + .map( + (completionPeriod) => + '${completionPeriod.unlockTime} - ${completionPeriod.lockTime}', + ) .join(','); } @@ -144,7 +163,10 @@ class _TaskList extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(AppLocalizations.of(context)!.tasks_daily, style: theme.textTheme.bodyMedium), + Text( + AppLocalizations.of(context)!.tasks_daily, + style: theme.textTheme.bodyMedium, + ), ], ), ), @@ -156,18 +178,30 @@ class _TaskList extends StatelessWidget { children: tasks .map( (task) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ - Expanded(child: Text(task.title!, style: theme.textTheme.bodyMedium)), + Expanded( + child: Text( + task.title!, + style: theme.textTheme.bodyMedium, + ), + ), Row( children: [ - Icon(Icons.access_time, size: 16, color: theme.textTheme.bodySmall!.color), + Icon( + Icons.access_time, + size: 16, + color: theme.textTheme.bodySmall!.color, + ), const SizedBox(width: 4), Text( scheduleString(task.schedule.completionPeriods), - style: theme.textTheme.bodyMedium! - .copyWith(fontSize: 12, color: theme.textTheme.bodySmall!.color), + style: theme.textTheme.bodyMedium!.copyWith( + fontSize: 12, + color: theme.textTheme.bodySmall!.color, + ), ), ], ), diff --git a/app/lib/widgets/questionnaire/audio_recording_question_widget.dart b/app/lib/widgets/questionnaire/audio_recording_question_widget.dart index 8466f9d81..7afbedd20 100644 --- a/app/lib/widgets/questionnaire/audio_recording_question_widget.dart +++ b/app/lib/widgets/questionnaire/audio_recording_question_widget.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:provider/provider.dart'; import 'package:record/record.dart'; import 'package:studyu_app/models/app_state.dart'; @@ -15,13 +15,19 @@ class AudioRecordingQuestionWidget extends QuestionWidget { final AudioRecordingQuestion question; final Function(Answer)? onDone; - const AudioRecordingQuestionWidget({super.key, required this.question, this.onDone}); + const AudioRecordingQuestionWidget({ + super.key, + required this.question, + this.onDone, + }); @override - State createState() => _AudioRecordingQuestionWidgetState(); + State createState() => + _AudioRecordingQuestionWidgetState(); } -class _AudioRecordingQuestionWidgetState extends State { +class _AudioRecordingQuestionWidgetState + extends State { bool _isRecording = false; bool _hasRecorded = false; late final AudioRecorder _audioRecorder; @@ -47,90 +53,105 @@ class _AudioRecordingQuestionWidgetState extends State(); - final maxRecordingDurationSeconds = widget.question.maxRecordingDurationSeconds; - return Row(children: [ - Expanded( + final maxRecordingDurationSeconds = + widget.question.maxRecordingDurationSeconds; + return Row( + children: [ + Expanded( child: OutlinedButton( - style: OutlinedButton.styleFrom( - backgroundColor: _isRecording ? Colors.red.shade600 : null, - foregroundColor: _isRecording ? Colors.white : theme.colorScheme.primary, - side: BorderSide( - width: 1.0, - color: _hasRecorded - ? Colors.black38 - : _isRecording - ? Colors.red.shade600 - : theme.colorScheme.primary), - ), - onPressed: !_hasRecorded - ? () async { - if (_isRecording) { - await _stopRecording(); - } else { - await _startRecording(appState.activeSubject!.studyId, appState.activeSubject!.userId); - } - } - : null, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 2.0, - ), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.all(4.0), - child: Icon( - _hasRecorded - ? MdiIcons.checkCircleOutline - : _isRecording - ? MdiIcons.stop - : MdiIcons.microphone, - color: _hasRecorded - ? Colors.black38 - : _isRecording - ? Colors.white - : theme.colorScheme.primary, - size: 24, - ), - ), - const Spacer(), - Text( - _hasRecorded - ? loc.audio_recorded + style: OutlinedButton.styleFrom( + backgroundColor: _isRecording ? Colors.red.shade600 : null, + foregroundColor: + _isRecording ? Colors.white : theme.colorScheme.primary, + side: BorderSide( + color: _hasRecorded + ? Colors.black38 : _isRecording - ? loc.stop_recording - : loc.start_recording, - style: const TextStyle(fontSize: 16), + ? Colors.red.shade600 + : theme.colorScheme.primary, + ), + ), + onPressed: !_hasRecorded + ? () async { + if (_isRecording) { + await _stopRecording(); + } else { + await _startRecording( + appState.activeSubject!.studyId, + appState.activeSubject!.userId, + ); + } + } + : null, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 2.0, ), - const Spacer(), - ], + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(4.0), + child: Icon( + _hasRecorded + ? MdiIcons.checkCircleOutline + : _isRecording + ? MdiIcons.stop + : MdiIcons.microphone, + color: _hasRecorded + ? Colors.black38 + : _isRecording + ? Colors.white + : theme.colorScheme.primary, + size: 24, + ), + ), + const Spacer(), + Text( + _hasRecorded + ? loc.audio_recorded + : _isRecording + ? loc.stop_recording + : loc.start_recording, + style: const TextStyle(fontSize: 16), + ), + const Spacer(), + ], + ), + ), ), ), - )), - const SizedBox(width: 16.0), - Text( - '${_formatNumber(_recordDurationSeconds ~/ 60)}:${_formatNumber(_recordDurationSeconds % 60)}', - style: const TextStyle(fontSize: 16), - ), - const SizedBox(width: 8.0), - _isRecording && _recordDurationSeconds > 0 && _recordDurationSeconds < maxRecordingDurationSeconds - ? SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - value: 1.0 - (_recordDurationSeconds / maxRecordingDurationSeconds), - strokeWidth: 2.5, - )) - : const SizedBox.shrink(), - ]); + const SizedBox(width: 16.0), + Text( + '${_formatNumber(_recordDurationSeconds ~/ 60)}:${_formatNumber(_recordDurationSeconds % 60)}', + style: const TextStyle(fontSize: 16), + ), + const SizedBox(width: 8.0), + if (_isRecording && + _recordDurationSeconds > 0 && + _recordDurationSeconds < maxRecordingDurationSeconds) + SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + value: + 1.0 - (_recordDurationSeconds / maxRecordingDurationSeconds), + strokeWidth: 2.5, + ), + ) + else + const SizedBox.shrink(), + ], + ); } Future _startRecording(String studyId, String userId) async { if (kIsWeb) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.multimodal_not_supported), - )); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.multimodal_not_supported), + ), + ); return; } @@ -148,8 +169,7 @@ class _AudioRecordingQuestionWidgetState extends State _recordDurationSeconds++); - if (_recordDurationSeconds >= widget.question.maxRecordingDurationSeconds) { + if (_recordDurationSeconds >= + widget.question.maxRecordingDurationSeconds) { await _stopRecording(); } }); diff --git a/app/lib/widgets/questionnaire/custom_slider.dart b/app/lib/widgets/questionnaire/custom_slider.dart index 72791237b..7c5c371da 100644 --- a/app/lib/widgets/questionnaire/custom_slider.dart +++ b/app/lib/widgets/questionnaire/custom_slider.dart @@ -22,36 +22,39 @@ class CustomSlider extends StatelessWidget { final bool linearStep; final AnnotatedScaleQuestion? steps; // nullable - const CustomSlider( - {super.key, - // required - this.value, - this.minValue, - this.maxValue, - //this.majorTick, - this.minorTick, - this.onChanged, - this.onChangeEnd, - // not required - this.activeColor, - this.inactiveColor, - this.minColor, - this.maxColor, - this.thumbColor, - this.isColored = false, - this.labelValuePrecision = 2, - this.tickValuePrecision = 1, - this.linearStep = true, - this.steps}); + const CustomSlider({ + super.key, + // required + this.value, + this.minValue, + this.maxValue, + //this.majorTick, + this.minorTick, + this.onChanged, + this.onChangeEnd, + // not required + this.activeColor, + this.inactiveColor, + this.minColor, + this.maxColor, + this.thumbColor, + this.isColored = false, + this.labelValuePrecision = 2, + this.tickValuePrecision = 1, + this.linearStep = true, + this.steps, + }); @override Widget build(BuildContext context) { final allocatedHeight = MediaQuery.of(context).size.height; - final allocatedWidth = MediaQuery.of(context).size.width - 32; // -32 horizontal padding + final allocatedWidth = + MediaQuery.of(context).size.width - 32; // -32 horizontal padding final divisions = (steps!.maximum - steps!.minimum) ~/ steps!.step; //final divisions = steps.annotations.length; // (majorTick - 1) * minorTick + majorTick; // final double valueHeight = allocatedHeight * 0.05 < 41 ? 41 : allocatedHeight * 0.05; - final double tickHeight = allocatedHeight * 0.0125 < 20 ? 20 : allocatedHeight * 0.0125; + final double tickHeight = + allocatedHeight * 0.0125 < 20 ? 20 : allocatedHeight * 0.0125; // todo finetune label positions final labelOffset = (allocatedWidth / (divisions + 2)) * 0.5; @@ -82,8 +85,11 @@ class CustomSlider extends StatelessWidget { return index + minValue! == value; } - String annotation(index) => annotations - .firstWhere((annotation) => annotation.value == index + minValue!, orElse: () => Annotation()) + String annotation(int index) => annotations + .firstWhere( + (annotation) => annotation.value == index + minValue!, + orElse: () => Annotation(), + ) .annotation; return Column( @@ -101,27 +107,35 @@ class CustomSlider extends StatelessWidget { }, child: Column( children: [ - index % (minorTick! + 1) == 0 && annotation(index).isNotEmpty - ? Container( - alignment: Alignment.bottomCenter, - //height: valueHeight, - child: Text( - //linearStep - // ? (index / (divisions - 1) * maxValue).toStringAsFixed(tickValuePrecision) - /*:*/ - annotation(index), - style: labelTextStyle! - .copyWith(fontWeight: isValueSelected(index) ? FontWeight.bold : FontWeight.normal), - textAlign: TextAlign.center, - )) - : const SizedBox.shrink(), + if (index % (minorTick! + 1) == 0 && + annotation(index).isNotEmpty) + Container( + alignment: Alignment.bottomCenter, + //height: valueHeight, + child: Text( + //linearStep + // ? (index / (divisions - 1) * maxValue).toStringAsFixed(tickValuePrecision) + /*:*/ + annotation(index), + style: labelTextStyle!.copyWith( + fontWeight: isValueSelected(index) + ? FontWeight.bold + : FontWeight.normal, + ), + textAlign: TextAlign.center, + ), + ) + else + const SizedBox.shrink(), Container( alignment: Alignment.bottomCenter, height: tickHeight, child: VerticalDivider( indent: index % (minorTick! + 1) == 0 ? 2 : 6, thickness: 1.8, - color: isValueSelected(index) ? thumbColor ?? primaryColor : Colors.grey.shade300, + color: isValueSelected(index) + ? thumbColor ?? primaryColor + : Colors.grey.shade300, ), ), ], @@ -134,7 +148,8 @@ class CustomSlider extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: labelOffset), child: SliderTheme( data: SliderThemeData( - trackHeight: allocatedHeight * 0.0125 < 9 ? 9 : allocatedHeight * 0.0125, + trackHeight: + allocatedHeight * 0.0125 < 9 ? 9 : allocatedHeight * 0.0125, inactiveTickMarkColor: isColored ? activeColor : null, activeTrackColor: activeColor, inactiveTrackColor: inactiveColor, @@ -152,7 +167,9 @@ class CustomSlider extends StatelessWidget { children: [ if (isColored) Container( - height: allocatedHeight * 0.0125 < 9 ? 9 : allocatedHeight * 0.0125, + height: allocatedHeight * 0.0125 < 9 + ? 9 + : allocatedHeight * 0.0125, decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(8)), gradient: LinearGradient( @@ -191,7 +208,8 @@ class CustomTrackShape extends RoundedRectSliderTrackShape { }) { final double trackHeight = sliderTheme.trackHeight!; final double trackLeft = offset.dx; - final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2; + final double trackTop = + offset.dy + (parentBox.size.height - trackHeight) / 2; final double trackWidth = parentBox.size.width; return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight); } diff --git a/app/lib/widgets/questionnaire/image_capturing_question_widget.dart b/app/lib/widgets/questionnaire/image_capturing_question_widget.dart index c6484766b..1378bcbf1 100644 --- a/app/lib/widgets/questionnaire/image_capturing_question_widget.dart +++ b/app/lib/widgets/questionnaire/image_capturing_question_widget.dart @@ -12,13 +12,19 @@ class ImageCapturingQuestionWidget extends QuestionWidget { final ImageCapturingQuestion question; final Function(Answer)? onDone; - const ImageCapturingQuestionWidget({super.key, required this.question, this.onDone}); + const ImageCapturingQuestionWidget({ + super.key, + required this.question, + this.onDone, + }); @override - State createState() => _ImageCapturingQuestionWidgetState(); + State createState() => + _ImageCapturingQuestionWidgetState(); } -class _ImageCapturingQuestionWidgetState extends State { +class _ImageCapturingQuestionWidgetState + extends State { bool _hasCaptured = false; @override @@ -32,9 +38,10 @@ class _ImageCapturingQuestionWidgetState extends State _captureImage() async { if (kIsWeb) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(AppLocalizations.of(context)!.multimodal_not_supported), - )); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.multimodal_not_supported), + ), + ); return; } final appState = context.read(); - FutureBlobFile? imageFile = await Navigator.push(context, MaterialPageRoute( - builder: (context) { - return CapturePictureScreen( - studyId: appState.activeSubject!.studyId, - userId: appState.activeSubject!.userId, - ); - }, - )); + final FutureBlobFile? imageFile = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return CapturePictureScreen( + studyId: appState.activeSubject!.studyId, + userId: appState.activeSubject!.userId, + ); + }, + ), + ); if (imageFile != null) { setState(() { _hasCaptured = true; diff --git a/app/lib/widgets/questionnaire/question_container.dart b/app/lib/widgets/questionnaire/question_container.dart index 2493fd7bf..47002622d 100644 --- a/app/lib/widgets/questionnaire/question_container.dart +++ b/app/lib/widgets/questionnaire/question_container.dart @@ -1,81 +1,89 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:studyu_app/widgets/questionnaire/audio_recording_question_widget.dart'; +import 'package:studyu_app/widgets/questionnaire/image_capturing_question_widget.dart'; +import 'package:studyu_app/widgets/questionnaire/question_header.dart'; +import 'package:studyu_app/widgets/questionnaire/questions/annotated_scale_question_widget.dart'; +import 'package:studyu_app/widgets/questionnaire/questions/boolean_question_widget.dart'; import 'package:studyu_app/widgets/questionnaire/questions/choice_question_widget.dart'; import 'package:studyu_app/widgets/questionnaire/questions/free_text_question_widget.dart'; +import 'package:studyu_app/widgets/questionnaire/questions/question_widget.dart'; import 'package:studyu_app/widgets/questionnaire/questions/scale_question_widget.dart'; -import 'package:studyu_app/widgets/questionnaire/image_capturing_question_widget.dart'; -import 'package:studyu_app/widgets/questionnaire/audio_recording_question_widget.dart'; +import 'package:studyu_app/widgets/questionnaire/questions/visual_analogue_question_widget.dart'; import 'package:studyu_core/core.dart'; -import 'questions/annotated_scale_question_widget.dart'; -import 'questions/boolean_question_widget.dart'; -import 'question_header.dart'; -import 'questions/question_widget.dart'; -import 'questions/visual_analogue_question_widget.dart'; - class QuestionContainer extends StatefulWidget { final Function(Answer, int) onDone; final Question question; final int index; - const QuestionContainer({required this.onDone, required this.question, required this.index, super.key}); + const QuestionContainer({ + required this.onDone, + required this.question, + required this.index, + super.key, + }); @override State createState() => _QuestionContainerState(); } -class _QuestionContainerState extends State with AutomaticKeepAliveClientMixin { +class _QuestionContainerState extends State + with AutomaticKeepAliveClientMixin { void _onDone(Answer answer) { widget.onDone(answer, widget.index); } QuestionWidget getQuestionBody(BuildContext context) { switch (widget.question) { - case ChoiceQuestion choiceQuestion: + case final ChoiceQuestion choiceQuestion: return ChoiceQuestionWidget( question: choiceQuestion, onDone: _onDone, - multiSelectionText: AppLocalizations.of(context)!.eligible_choice_multi_selection, + multiSelectionText: + AppLocalizations.of(context)!.eligible_choice_multi_selection, ); - case BooleanQuestion booleanQuestion: + case final BooleanQuestion booleanQuestion: return BooleanQuestionWidget( question: booleanQuestion, onDone: _onDone, ); - case ScaleQuestion scaleQuestion: + case final ScaleQuestion scaleQuestion: return ScaleQuestionWidget( question: scaleQuestion, onDone: _onDone, ); - case ImageCapturingQuestion imageCapturingQuestion: + case final ImageCapturingQuestion imageCapturingQuestion: return ImageCapturingQuestionWidget( question: imageCapturingQuestion, onDone: _onDone, ); - case AudioRecordingQuestion audioRecordingQuestion: + case final AudioRecordingQuestion audioRecordingQuestion: return AudioRecordingQuestionWidget( question: audioRecordingQuestion, onDone: _onDone, ); - case VisualAnalogueQuestion visualAnalogueQuestion: + case final VisualAnalogueQuestion visualAnalogueQuestion: // todo remove this when older studies are finished // ignore: deprecated_member_use_from_same_package return VisualAnalogueQuestionWidget( question: visualAnalogueQuestion, onDone: _onDone, ); - case AnnotatedScaleQuestion annotatedScaleQuestion: + case final AnnotatedScaleQuestion annotatedScaleQuestion: return AnnotatedScaleQuestionWidget( question: annotatedScaleQuestion, onDone: _onDone, ); - case FreeTextQuestion freeTextQuestion: + case final FreeTextQuestion freeTextQuestion: return FreeTextQuestionWidget( question: freeTextQuestion, onDone: _onDone, ); default: - throw ArgumentError('Question type ${widget.question.type} not supported'); + throw ArgumentError( + 'Question type ${widget.question.type} not supported', + ); } } diff --git a/app/lib/widgets/questionnaire/question_header.dart b/app/lib/widgets/questionnaire/question_header.dart index c340978c9..797c45d49 100644 --- a/app/lib/widgets/questionnaire/question_header.dart +++ b/app/lib/widgets/questionnaire/question_header.dart @@ -30,7 +30,7 @@ class QuestionHeader extends StatelessWidget { content: HtmlText(rationale), ), ), - ) + ), ]; } diff --git a/app/lib/widgets/questionnaire/questionnaire_widget.dart b/app/lib/widgets/questionnaire/questionnaire_widget.dart index d7d67f0a6..c0f12d7f5 100644 --- a/app/lib/widgets/questionnaire/questionnaire_widget.dart +++ b/app/lib/widgets/questionnaire/questionnaire_widget.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:studyu_app/widgets/html_text.dart'; +import 'package:studyu_app/widgets/questionnaire/question_container.dart'; import 'package:studyu_core/core.dart'; -import 'question_container.dart'; - typedef StateHandler = void Function(QuestionnaireState); typedef ContinuationPredicate = bool Function(QuestionnaireState); @@ -40,13 +39,18 @@ class _QuestionnaireWidgetState extends State { final QuestionnaireState qs = QuestionnaireState(); int _nextQuestionIndex = 1; - void _finishQuestionnaire(QuestionnaireState result) => widget.onComplete?.call(result); + void _finishQuestionnaire(QuestionnaireState result) => + widget.onComplete?.call(result); // if question with lower index than current question is answered, remove all downstream answers void _invalidateDownstreamAnswers(int index) { if (index < shownQuestions.length - 1) { - final startIndex = widget.questions.indexWhere((question) => question.id == shownQuestions[index].question.id); - widget.questions.skip(startIndex + 1).forEach((question) => qs.answers.remove(question.id)); + final startIndex = widget.questions.indexWhere( + (question) => question.id == shownQuestions[index].question.id, + ); + widget.questions + .skip(startIndex + 1) + .forEach((question) => qs.answers.remove(question.id)); while (index + 1 < shownQuestions.length) { final end = shownQuestions.length; final lastQuestion = shownQuestions.removeLast(); @@ -73,13 +77,16 @@ class _QuestionnaireWidgetState extends State { } void _onQuestionDone(Answer answer, int index) { - _nextQuestionIndex = widget.questions.indexWhere((question) => question.id == answer.question) + 1; + _nextQuestionIndex = widget.questions + .indexWhere((question) => question.id == answer.question) + + 1; qs.answers[answer.question] = answer; widget.onChange?.call(qs); final shouldContinue = widget.shouldContinue?.call(qs); // only invalidate if there is a conditional question or if answers do not allow to continue - if (shownQuestions.any((element) => element.question.conditional != null) || shouldContinue == false) { + if (shownQuestions.any((element) => element.question.conditional != null) || + shouldContinue == false) { _invalidateDownstreamAnswers(index); } @@ -94,11 +101,16 @@ class _QuestionnaireWidgetState extends State { // check for conditional questions if (!widget.questions[_nextQuestionIndex].shouldBeShown(qs)) { - _onQuestionDone(widget.questions[_nextQuestionIndex].getDefaultAnswer()!, shownQuestions.length); + _onQuestionDone( + widget.questions[_nextQuestionIndex].getDefaultAnswer()!, + shownQuestions.length, + ); return; } _insertQuestion(widget.questions[_nextQuestionIndex]); - _listKey.currentState!.insertItem(shownQuestions.length - 1, duration: const Duration(milliseconds: 300)); + _listKey.currentState!.insertItem( + shownQuestions.length - 1, + ); _nextQuestionIndex++; } else { // we ran out of questions @@ -129,11 +141,16 @@ class _QuestionnaireWidgetState extends State { initialItemCount: shownQuestions.length + 2, itemBuilder: (context, index, animation) { if (index == 0) { - return widget.header != null && widget.header!.isNotEmpty ? HtmlTextBox(widget.header) : Container(); + return widget.header != null && widget.header!.isNotEmpty + ? HtmlTextBox(widget.header) + : Container(); } index -= 1; - if (index == widget.questions.length && qs.answers.length == widget.questions.length) { - return widget.footer != null && widget.footer!.isNotEmpty ? HtmlTextBox(widget.footer) : Container(); + if (index == widget.questions.length && + qs.answers.length == widget.questions.length) { + return widget.footer != null && widget.footer!.isNotEmpty + ? HtmlTextBox(widget.footer) + : Container(); } if (index > shownQuestions.length - 1) { return Container(); diff --git a/app/lib/widgets/questionnaire/questions/annotated_scale_question_widget.dart b/app/lib/widgets/questionnaire/questions/annotated_scale_question_widget.dart index ff98fd931..deeceb822 100644 --- a/app/lib/widgets/questionnaire/questions/annotated_scale_question_widget.dart +++ b/app/lib/widgets/questionnaire/questions/annotated_scale_question_widget.dart @@ -1,21 +1,26 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:studyu_app/widgets/questionnaire/custom_slider.dart'; +import 'package:studyu_app/widgets/questionnaire/questions/question_widget.dart'; import 'package:studyu_core/core.dart'; -import '../custom_slider.dart'; -import 'question_widget.dart'; - class AnnotatedScaleQuestionWidget extends QuestionWidget { final AnnotatedScaleQuestion question; final Function(Answer)? onDone; - const AnnotatedScaleQuestionWidget({super.key, required this.question, this.onDone}); + const AnnotatedScaleQuestionWidget({ + super.key, + required this.question, + this.onDone, + }); @override - State createState() => _AnnotatedScaleQuestionWidgetState(); + State createState() => + _AnnotatedScaleQuestionWidgetState(); } -class _AnnotatedScaleQuestionWidgetState extends State { +class _AnnotatedScaleQuestionWidgetState + extends State { double? value; late bool sliderTouched; @@ -65,7 +70,7 @@ class _AnnotatedScaleQuestionWidgetState extends State createState() => _ChoiceQuestionWidgetState(); @@ -61,8 +65,10 @@ class _ChoiceQuestionWidgetState extends State { OutlinedButton( onPressed: confirm, style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.secondary), - foregroundColor: MaterialStateProperty.all(Colors.white), + backgroundColor: WidgetStateProperty.all( + Theme.of(context).colorScheme.secondary, + ), + foregroundColor: WidgetStateProperty.all(Colors.white), ), child: Text(AppLocalizations.of(context)!.confirm), ), diff --git a/app/lib/widgets/questionnaire/questions/free_text_question_widget.dart b/app/lib/widgets/questionnaire/questions/free_text_question_widget.dart index 5d3e558d4..cf5a71d6c 100644 --- a/app/lib/widgets/questionnaire/questions/free_text_question_widget.dart +++ b/app/lib/widgets/questionnaire/questions/free_text_question_widget.dart @@ -7,7 +7,11 @@ class FreeTextQuestionWidget extends QuestionWidget { final FreeTextQuestion question; final Function(Answer)? onDone; - const FreeTextQuestionWidget({super.key, required this.question, this.onDone}); + const FreeTextQuestionWidget({ + super.key, + required this.question, + this.onDone, + }); @override State createState() => _FreeTextQuestionWidgetState(); @@ -44,9 +48,11 @@ class _FreeTextQuestionWidgetState extends State { }, validator: (value) { if (value!.length < question.lengthRange.first) { - return AppLocalizations.of(context)!.free_text_min_length_error(question.lengthRange.first); + return AppLocalizations.of(context)! + .free_text_min_length_error(question.lengthRange.first); } else if (value.length > question.lengthRange.last) { - return AppLocalizations.of(context)!.free_text_max_length_error(question.lengthRange.last); + return AppLocalizations.of(context)! + .free_text_max_length_error(question.lengthRange.last); } switch (question.textType) { case FreeTextQuestionType.any: @@ -67,7 +73,8 @@ class _FreeTextQuestionWidgetState extends State { if (RegExp(question.customTypeExpression!).hasMatch(value)) { return null; } else { - return AppLocalizations.of(context)!.free_text_custom_error(question.customTypeExpression!); + return AppLocalizations.of(context)! + .free_text_custom_error(question.customTypeExpression!); } } }, diff --git a/app/lib/widgets/questionnaire/questions/scale_question_widget.dart b/app/lib/widgets/questionnaire/questions/scale_question_widget.dart index 89d5de6ee..7cb147592 100644 --- a/app/lib/widgets/questionnaire/questions/scale_question_widget.dart +++ b/app/lib/widgets/questionnaire/questions/scale_question_widget.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:studyu_app/theme.dart'; +import 'package:studyu_app/widgets/questionnaire/custom_slider.dart'; +import 'package:studyu_app/widgets/questionnaire/questions/question_widget.dart'; import 'package:studyu_core/core.dart'; -import '../custom_slider.dart'; -import 'question_widget.dart'; - class ScaleQuestionWidget extends QuestionWidget { final ScaleQuestion question; final Function(Answer)? onDone; @@ -29,10 +28,15 @@ class _ScaleQuestionWidgetState extends State { @override Widget build(BuildContext context) { - final sliderRange = (widget.question.maximum - widget.question.minimum).abs(); + final sliderRange = + (widget.question.maximum - widget.question.minimum).abs(); - Color? minColor = widget.question.minColor != null ? Color(widget.question.minColor!) : null; - Color? maxColor = widget.question.maxColor != null ? Color(widget.question.maxColor!) : null; + Color? minColor = widget.question.minColor != null + ? Color(widget.question.minColor!) + : null; + Color? maxColor = widget.question.maxColor != null + ? Color(widget.question.maxColor!) + : null; final isColored = minColor != null || maxColor != null; if (isColored) { @@ -44,48 +48,56 @@ class _ScaleQuestionWidgetState extends State { final theme = Theme.of(context); final coloredSliderTheme = ThemeConfig.coloredSliderTheme(theme); final thumbColor = isColored - ? Color.lerp(minColor, maxColor, (value! - widget.question.minimum) / sliderRange)!.withOpacity(1) + ? Color.lerp( + minColor, + maxColor, + (value! - widget.question.minimum) / sliderRange, + )! + .withOpacity(1) : null; - final activeTrackColor = isColored ? coloredSliderTheme.activeTrackColor : null; - final inactiveTrackColor = isColored ? coloredSliderTheme.inactiveTrackColor : null; + final activeTrackColor = + isColored ? coloredSliderTheme.activeTrackColor : null; + final inactiveTrackColor = + isColored ? coloredSliderTheme.inactiveTrackColor : null; return Column( children: [ Stack( children: [ Theme( - data: isColored - ? theme.copyWith( - sliderTheme: SliderThemeData( - overlayColor: thumbColor!.withOpacity(0.5), - ), - ) - : theme, - child: CustomSlider( - minValue: widget.question.minimum, - maxValue: widget.question.maximum, - value: value, - minorTick: 0, - labelValuePrecision: 0, - tickValuePrecision: 0, - onChanged: (val) => setState(() { - value = val; - //print('Slider value (linear): $value'); - }), - onChangeEnd: (val) => setState(() { - value = val; - sliderTouched = true; - widget.onDone!(widget.question.constructAnswer(value!)); - }), - activeColor: activeTrackColor, - inactiveColor: inactiveTrackColor, - thumbColor: thumbColor, - minColor: minColor, - maxColor: maxColor, - isColored: isColored, - linearStep: false, - steps: widget.question, - )), + data: isColored + ? theme.copyWith( + sliderTheme: SliderThemeData( + overlayColor: thumbColor!.withOpacity(0.5), + ), + ) + : theme, + child: CustomSlider( + minValue: widget.question.minimum, + maxValue: widget.question.maximum, + value: value, + minorTick: 0, + labelValuePrecision: 0, + tickValuePrecision: 0, + onChanged: (val) => setState(() { + value = val; + //print('Slider value (linear): $value'); + }), + onChangeEnd: (val) => setState(() { + value = val; + sliderTouched = true; + widget.onDone!(widget.question.constructAnswer(value!)); + }), + activeColor: activeTrackColor, + inactiveColor: inactiveTrackColor, + thumbColor: thumbColor, + minColor: minColor, + maxColor: maxColor, + isColored: isColored, + linearStep: false, + steps: widget.question, + ), + ), ], ), if (!sliderTouched) @@ -100,7 +112,7 @@ class _ScaleQuestionWidgetState extends State { }, child: Text(AppLocalizations.of(context)!.done), ), - ) + ), ], ); } diff --git a/app/lib/widgets/questionnaire/questions/visual_analogue_question_widget.dart b/app/lib/widgets/questionnaire/questions/visual_analogue_question_widget.dart index 98e260b52..04f96fbc9 100644 --- a/app/lib/widgets/questionnaire/questions/visual_analogue_question_widget.dart +++ b/app/lib/widgets/questionnaire/questions/visual_analogue_question_widget.dart @@ -1,22 +1,28 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:studyu_app/widgets/questionnaire/questions/question_widget.dart'; import 'package:studyu_core/core.dart'; -import 'question_widget.dart'; - @Deprecated('Use [AnnotatedScaleQuestionWidget]') class VisualAnalogueQuestionWidget extends QuestionWidget { final VisualAnalogueQuestion question; final Function(Answer)? onDone; - const VisualAnalogueQuestionWidget({super.key, required this.question, this.onDone}); + @Deprecated('Use [AnnotatedScaleQuestionWidget]') + const VisualAnalogueQuestionWidget({ + super.key, + required this.question, + this.onDone, + }); @override - State createState() => _VisualAnalogueQuestionWidgetState(); + State createState() => + _VisualAnalogueQuestionWidgetState(); } @Deprecated('Use [_AnnotatedScaleQuestionWidgetState]') -class _VisualAnalogueQuestionWidgetState extends State { +class _VisualAnalogueQuestionWidgetState + extends State { late double value; @override @@ -46,7 +52,10 @@ class _VisualAnalogueQuestionWidgetState extends State widget.onDone!(widget.question.constructAnswer(value)), + onPressed: () => + widget.onDone!(widget.question.constructAnswer(value)), child: Text(AppLocalizations.of(context)!.done), ), - ) + ), ], ); } diff --git a/app/lib/widgets/selectable_button.dart b/app/lib/widgets/selectable_button.dart index 7d601013a..b6d5c0e89 100644 --- a/app/lib/widgets/selectable_button.dart +++ b/app/lib/widgets/selectable_button.dart @@ -5,9 +5,15 @@ class SelectableButton extends StatelessWidget { final bool selected; final Function()? onTap; - const SelectableButton({super.key, required this.child, this.selected = false, this.onTap}); + const SelectableButton({ + super.key, + required this.child, + this.selected = false, + this.onTap, + }); - Color _getFillColor(ThemeData theme) => selected ? theme.primaryColor : theme.cardColor; + Color _getFillColor(ThemeData theme) => + selected ? theme.primaryColor : theme.cardColor; Color _getTextColor(ThemeData theme) => selected ? Colors.white : Colors.blue; diff --git a/app/lib/widgets/study_tile.dart b/app/lib/widgets/study_tile.dart index 4a050426b..9650ec40b 100644 --- a/app/lib/widgets/study_tile.dart +++ b/app/lib/widgets/study_tile.dart @@ -20,8 +20,12 @@ class StudyTile extends StatelessWidget { super.key, }); - StudyTile.fromStudy({required Study study, this.onTap, this.contentPadding = const EdgeInsets.all(16), super.key}) - : title = study.title, + StudyTile.fromStudy({ + required Study study, + this.onTap, + this.contentPadding = const EdgeInsets.all(16), + super.key, + }) : title = study.title, description = study.description, iconName = study.iconName; @@ -43,9 +47,16 @@ class StudyTile extends StatelessWidget { ListTile( contentPadding: contentPadding, onTap: onTap, - title: Center(child: Text(title!, style: theme.textTheme.titleLarge!.copyWith(color: theme.primaryColor))), + title: Center( + child: Text( + title!, + style: theme.textTheme.titleLarge! + .copyWith(color: theme.primaryColor), + ), + ), subtitle: Center(child: Text(description ?? '')), - leading: Icon(MdiIcons.fromString(iconName), color: theme.primaryColor), + leading: + Icon(MdiIcons.fromString(iconName), color: theme.primaryColor), ), ], ); diff --git a/app/pubspec.lock b/app/pubspec.lock index 8fda1fbd1..1beac440d 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: app_links - sha256: "0fd41f0501f131d931251e0942ac63d6216096a0052aeca037915c2c1deeb121" + sha256: "96e677810b83707ff5e10fac11e4839daa0ea4e0123c35864c092699165eb3db" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.1.1" archive: dependency: transitive description: name: archive - sha256: "0763b45fa9294197a2885c8567927e2830ade852e5c896fd4ab7e0e348d0f373" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.5.0" + version: "3.6.1" args: dependency: transitive description: @@ -93,18 +93,18 @@ packages: dependency: "direct main" description: name: camera - sha256: dfa8fc5a1adaeb95e7a54d86a5bd56f4bb0e035515354c8ac6d262e35cec2ec8 + sha256: "2170a943dcb67be2af2c6bcda8775e74b41d4c02d6a4eb10bdc832ee185c4eea" url: "https://pub.dev" source: hosted - version: "0.10.6" - camera_android: + version: "0.11.0+1" + camera_android_camerax: dependency: transitive description: - name: camera_android - sha256: "7b0aba6398afa8475e2bc9115d976efb49cf8db781e922572d443795c04a4f4f" + name: camera_android_camerax + sha256: "7d84815edbb8304b51c10deba3c20f44eef80aa46ff156ec45428ed16600b49a" url: "https://pub.dev" source: hosted - version: "0.10.9+1" + version: "0.6.5+5" camera_avfoundation: dependency: transitive description: @@ -359,18 +359,18 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" + sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "2.0.20" flutter_secure_storage: dependency: transitive description: name: flutter_secure_storage - sha256: c0f402067fb0498934faa6bddd670de0a3db45222e2ca9a068c6177c9a2360a4 + sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0" url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "9.2.2" flutter_secure_storage_linux: dependency: transitive description: @@ -383,18 +383,18 @@ packages: dependency: transitive description: name: flutter_secure_storage_macos - sha256: "8cfa53010a294ff095d7be8fa5bb15f2252c50018d69c5104851303f3ff92510" + sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: "301f67ee9b87f04aef227f57f13f126fa7b13543c8e7a93f25c5d2d534c28a4a" + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" flutter_secure_storage_web: dependency: transitive description: @@ -441,82 +441,82 @@ packages: dependency: "direct main" description: name: flutter_widget_from_html - sha256: b931148824bcfc24c7be6f2daeabf549e237c9e617e93cd63256d924884ce423 + sha256: e79144d8a37b7d1075fc1fdebc32708bd142ad9fdf1c7d9444f5e964cc03158b url: "https://pub.dev" source: hosted - version: "0.15.0" + version: "0.15.1" flutter_widget_from_html_core: dependency: transitive description: name: flutter_widget_from_html_core - sha256: cc1d9be3d187ce668ee02091cd5442dfb050cdaf98e0ab9a4d12ad008f966979 + sha256: df7c7c9e5ea144f7ab0adfbad733b4d4f7d408ab733c94e6e9fdcb327af92aa1 url: "https://pub.dev" source: hosted - version: "0.14.12" + version: "0.15.1" functions_client: dependency: transitive description: name: functions_client - sha256: a70b0dd9a1c35d05d1141557f7e49ffe4de5f450ffde31755a9eeeadca03b8ee + sha256: "48659e5c6a4bbe02659102bf6406a0cf39142202deae65aacfa78688f2e68946" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" fwfh_cached_network_image: dependency: transitive description: name: fwfh_cached_network_image - sha256: "952aea958a5fda7d616cc297ba4bc08427e381459e75526fa375d6d8345630d3" + sha256: "8e44226801bfba27930673953afce8af44da7e92573be93f60385d9865a089dd" url: "https://pub.dev" source: hosted - version: "0.14.2" + version: "0.14.3" fwfh_chewie: dependency: transitive description: name: fwfh_chewie - sha256: bbb036cd322ab77dc0edd34cbbf76181681f5e414987ece38745dc4f3d7408ed + sha256: "37bde9cedfb6dc5546176f7f0c56af1e814966cb33ec58f16c9565ed93ccb704" url: "https://pub.dev" source: hosted - version: "0.14.7" + version: "0.14.8" fwfh_just_audio: dependency: transitive description: name: fwfh_just_audio - sha256: "209cf9644599e37b0edb6961c4f30ce80d156f5a53a50355f75fb4a22f9fdb0a" + sha256: "4ff7927619ff4855567a61e126269e1fef985a9fe7e78682592da17bf658aabb" url: "https://pub.dev" source: hosted - version: "0.14.3" + version: "0.15.1" fwfh_svg: dependency: transitive description: name: fwfh_svg - sha256: "3fd83926b7245d287f133a437ef430befd99d3b00ba8c600f26cc324af281f72" + sha256: c6bb6b513f7ce2766aba76d7276caf9a96b6fee729ac3a492c366a42f82ef02e url: "https://pub.dev" source: hosted - version: "0.8.1" + version: "0.8.2" fwfh_url_launcher: dependency: transitive description: name: fwfh_url_launcher - sha256: "2a526c9819f74b4106ba2fba4dac79f0082deecd8d2c7011cd0471cb710e3eff" + sha256: b9f5d55a5ae2c2c07243ba33f7ba49ac9544bdb2f4c16d8139df9ccbebe3449c url: "https://pub.dev" source: hosted - version: "0.9.0+4" + version: "0.9.1" fwfh_webview: dependency: transitive description: name: fwfh_webview - sha256: "63fa69577d5cbed5603cc91d6a7eca62472a852db14225da2f4ec6839c2f59e6" + sha256: "2cd2b1e463ddaf26b7d4f74e1a855126c4a836fdaff9551636693e07a07422b6" url: "https://pub.dev" source: hosted - version: "0.15.0" + version: "0.15.1" gotrue: dependency: transitive description: name: gotrue - sha256: c9c984f088320a5c5e87c7a34571e3de3982cca4cbd8b978e59d36baf748edfb + sha256: b7324a9113bb21c354fd52531d9a9dba6570a9c37b2fc6b424865feb19974476 url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.8.0" gtk: dependency: transitive description: @@ -553,10 +553,10 @@ packages: dependency: transitive description: name: image - sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" url: "https://pub.dev" source: hosted - version: "4.1.7" + version: "4.2.0" intersperse: dependency: "direct main" description: @@ -593,26 +593,26 @@ packages: dependency: transitive description: name: just_audio - sha256: b7cb6bbf3750caa924d03f432ba401ec300fd90936b3f73a9b33d58b1e96286b + sha256: "5abfab1d199e01ab5beffa61b3e782350df5dad036cb8c83b79fa45fc656614e" url: "https://pub.dev" source: hosted - version: "0.9.37" + version: "0.9.38" just_audio_platform_interface: dependency: transitive description: name: just_audio_platform_interface - sha256: c3dee0014248c97c91fe6299edb73dc4d6c6930a2f4f713579cd692d9e47f4a1 + sha256: "0243828cce503c8366cc2090cefb2b3c871aa8ed2f520670d76fd47aa1ab2790" url: "https://pub.dev" source: hosted - version: "4.2.2" + version: "4.3.0" just_audio_web: dependency: transitive description: name: just_audio_web - sha256: "134356b0fe3d898293102b33b5fd618831ffdc72bb7a1b726140abdf22772b70" + sha256: "0edb481ad4aa1ff38f8c40f1a3576013c3420bf6669b686fe661627d49bc606c" url: "https://pub.dev" source: hosted - version: "0.4.9" + version: "0.4.11" jwt_decode: dependency: transitive description: @@ -625,26 +625,34 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" + lint: + dependency: "direct dev" + description: + name: lint + sha256: d758a5211fce7fd3f5e316f804daefecdc34c7e53559716125e6da7388ae8565 + url: "https://pub.dev" + source: hosted + version: "2.3.0" lints: dependency: transitive description: @@ -697,10 +705,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mime: dependency: transitive description: @@ -769,18 +777,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.5" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -825,18 +833,18 @@ packages: dependency: transitive description: name: permission_handler_android - sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474" + sha256: b29a799ca03be9f999aa6c39f7de5209482d638e6f857f6b93b0875c618b7e54 url: "https://pub.dev" source: hosted - version: "12.0.5" + version: "12.0.7" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662 + sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 url: "https://pub.dev" source: hosted - version: "9.4.4" + version: "9.4.5" permission_handler_html: dependency: transitive description: @@ -873,10 +881,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -889,10 +897,10 @@ packages: dependency: transitive description: name: postgrest - sha256: "9a3b590cf123f8d323b6a918702e037f037027d12a01902f9dc6ee38fdc05d6c" + sha256: f1f78470a74c611811132ff12acdef9c08b3ec65b61e88161a057d6cc5fbbd83 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" provider: dependency: "direct main" description: @@ -937,34 +945,34 @@ packages: dependency: transitive description: name: realtime_client - sha256: "492a1ab568b3812cb345aad8dd09b3936877edba81a6ab6f5fdf365c155797e1" + sha256: cd44fa21407a2e217d674f1c1a33b36c49ad0d8aea0349bf5b66594db06c80fb url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.1.0" record: dependency: "direct main" description: name: record - sha256: "113b368168c49c78902ab37c2b354dea30a0aec5bdeca434073826b6ea73eca1" + sha256: "78353d3d55fa145ffe1db1f63232ad0a0cd4c773e9f7d161210ce796ba1c94f9" url: "https://pub.dev" source: hosted - version: "5.0.5" + version: "5.1.1" record_android: dependency: transitive description: name: record_android - sha256: "0df98e05873b22b443309e289bf1eb3b5b9a60e7779134334e2073eb0763a992" + sha256: fe83beefc8ac81b9dd02ca9365e8685755e3f12be1d442964082f1d5b618183d url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.2" record_darwin: dependency: transitive description: name: record_darwin - sha256: ee8cb1bb1712d7ce38140ecabe70e5c286c02f05296d66043bee865ace7eb1b9 + sha256: "2210da0fde7c86b4048cccfe2cd19b25fc7adf1ada7d50ec4a5ab4af2a863739" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" record_linux: dependency: transitive description: @@ -977,26 +985,26 @@ packages: dependency: transitive description: name: record_platform_interface - sha256: "3a4b56e94ecd2a0b2b43eb1fa6f94c5b8484334f5d38ef43959c4bf97fb374cf" + sha256: "11f8b03ea8a0e279b0e306571dbe0db0202c0b8e866495c9fa1ad2281d5e4c15" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" record_web: dependency: transitive description: name: record_web - sha256: "24847cdbcf999f7a5762170792f622ac844858766becd0f2370ec8ae22f7526e" + sha256: "703adb626d31e2dd86a8f6b34e306e03cd323e0c5e16e11bbc0385b07a8df97e" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.1.1" record_windows: dependency: transitive description: name: record_windows - sha256: "39998b3ea7d8d28b04159d82220e6e5e32a7c357c6fb2794f5736beea272f6c3" + sha256: e653555aa3fda168aded7c34e11bd82baf0c6ac84e7624553def3c77ffefd36f url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" retry: dependency: transitive description: @@ -1017,26 +1025,26 @@ packages: dependency: transitive description: name: sentry - sha256: "1d2952d40b99da0dc4bf3ba4797e3985dd60cc61a13d0a1d2c62b02f6528441a" + sha256: fd1fbfe860c05f5c52820ec4dbf2b6473789e83ead26cfc18bca4fe80bf3f008 url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.2.0" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: "848aaccfc75f1d35d5f7e5230770761f1b2a33e604f24498200566443da1470a" + sha256: c64f0aec5332bec87083b61514d1b6b29e435b9045d03ce1575861192b9a5680 url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.2.0" sentry_logging: dependency: "direct main" description: name: sentry_logging - sha256: "86a0b30a868cd9738694ddde4b82a2ac60b1e33abdff04216dfc556ef6d56fb1" + sha256: edfc054d65f0257303540f9fe76820005a710440eee062052efdff5252ecc121 url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.2.0" shared_preferences: dependency: "direct main" description: @@ -1049,18 +1057,18 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.4.0" shared_preferences_linux: dependency: transitive description: @@ -1118,10 +1126,10 @@ packages: dependency: transitive description: name: sqflite - sha256: "5ce2e1a15e822c3b4bfb5400455775e421da7098eed8adc8f26298ada7c9308c" + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.3+1" sqflite_common: dependency: transitive description: @@ -1142,10 +1150,10 @@ packages: dependency: transitive description: name: storage_client - sha256: bf5589d5de61a2451edb1b8960a0e673d4bb5c42ecc4dddf7c051a93789ced34 + sha256: e37f1b9d40f43078d12bd2d1b6b08c2c16fbdbafc58b57bc44922da6ea3f5625 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" stream_channel: dependency: transitive description: @@ -1176,30 +1184,30 @@ packages: path: "../core" relative: true source: path - version: "4.4.2" + version: "4.4.3-dev.1" studyu_flutter_common: dependency: "direct main" description: path: "../flutter_common" relative: true source: path - version: "1.8.3" + version: "1.8.4-dev.1" supabase: dependency: "direct main" description: name: supabase - sha256: ef407187b18c440f4a5c3f3cf30eb5cc1daadd4ff5616febf445a37e0e0ed34e + sha256: "1133466d2ec9441e7cff6bf73943f0a6e17cbb5044f6327ca93e3dddc567c882" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.2.1" supabase_flutter: dependency: "direct main" description: name: supabase_flutter - sha256: "6a77bd6ef6dc451bb2561de0334d68d620b84d7df1de1448dd7962ed5d1a79ea" + sha256: "521de6a72fefc0447bd052b0a33be30ed429ac8c39d9028dfbf0f9af2c4c93b2" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.5" synchronized: dependency: transitive description: @@ -1220,10 +1228,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" timeline_tile: dependency: "direct main" description: @@ -1268,26 +1276,26 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.2.6" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" + sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.3" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" + sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.3.0" url_launcher_linux: dependency: transitive description: @@ -1300,10 +1308,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.0" url_launcher_platform_interface: dependency: transitive description: @@ -1372,26 +1380,26 @@ packages: dependency: transitive description: name: video_player - sha256: db6a72d8f4fd155d0189845678f55ad2fd54b02c10dcafd11c068dbb631286c0 + sha256: aced48e701e24c02b0b7f881a8819e4937794e46b5a5821005e2bf3b40a324cc url: "https://pub.dev" source: hosted - version: "2.8.6" + version: "2.8.7" video_player_android: dependency: transitive description: name: video_player_android - sha256: "134e1ad410d67e18a19486ed9512c72dfc6d8ffb284d0e8f2e99e903d1ba8fa3" + sha256: "9529001630e42988f755772972d5014d30121610700e8e502278a245939f8fc8" url: "https://pub.dev" source: hosted - version: "2.4.14" + version: "2.5.0" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "00c49b1d68071341397cf760b982c1e26ed9232464c8506ee08378a5cca5070d" + sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c url: "https://pub.dev" source: hosted - version: "2.5.7" + version: "2.6.1" video_player_platform_interface: dependency: transitive description: @@ -1404,18 +1412,18 @@ packages: dependency: transitive description: name: video_player_web - sha256: "41245cef5ef29c4585dbabcbcbe9b209e34376642c7576cabf11b4ad9289d6e4" + sha256: ff4d69a6614b03f055397c27a71c9d3ddea2b2a23d71b2ba0164f59ca32b8fe2 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" vm_service: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" wakelock_plus: dependency: "direct main" description: @@ -1452,18 +1460,18 @@ packages: dependency: transitive description: name: webview_flutter - sha256: "25e1b6e839e8cbfbd708abc6f85ed09d1727e24e08e08c6b8590d7c65c9a8932" + sha256: "6869c8786d179f929144b4a1f86e09ac0eddfe475984951ea6c634774c16b522" url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.8.0" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: f038ee2fae73b509dde1bc9d2c5a50ca92054282de17631a9a3d515883740934 + sha256: f42447ca49523f11d8f70abea55ea211b3cafe172dd7a0e7ac007bb35dd356dc url: "https://pub.dev" source: hosted - version: "3.16.0" + version: "3.16.4" webview_flutter_platform_interface: dependency: transitive description: @@ -1476,18 +1484,18 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: f12f8d8a99784b863e8b85e4a9a5e3cf1839d6803d2c0c3e0533a8f3c5a992a7 + sha256: "7affdf9d680c015b11587181171d3cad8093e449db1f7d9f0f08f4f33d24f9a0" url: "https://pub.dev" source: hosted - version: "3.13.0" + version: "3.13.1" win32: dependency: transitive description: name: win32 - sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.5.1" xdg_directories: dependency: transitive description: @@ -1521,5 +1529,5 @@ packages: source: hosted version: "2.0.0" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 8599dffca..9e253d580 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -1,5 +1,5 @@ name: studyu_app -version: 2.7.5 +version: 2.7.6-dev.2 description: Partake in digital N-of-1 trials with the innovative StudyU Health App publish_to: none homepage: https://studyu.health @@ -7,8 +7,9 @@ repository: https://github.com/hpi-studyu/studyu environment: sdk: '>=3.2.0 <4.0.0' + dependencies: - camera: ^0.10.6 + camera: ^0.11.0+1 collection: ^1.18.0 cross_file: ^0.3.4+1 cupertino_icons: ^1.0.8 @@ -22,7 +23,7 @@ dependencies: flutter_timezone: ^1.0.8 flutter_web_plugins: sdk: flutter - flutter_widget_from_html: ^0.15.0 + flutter_widget_from_html: ^0.15.1 intersperse: ^2.0.0 intl: ^0.19.0 material_design_icons_flutter: ^7.0.7296 @@ -34,18 +35,18 @@ dependencies: provider: ^6.1.2 quiver: ^3.2.1 rainbow_color: ^2.0.1 - record: ^5.0.5 - sentry_flutter: ^8.1.0 - sentry_logging: ^8.1.0 + record: ^5.1.1 + sentry_flutter: ^8.2.0 + sentry_logging: ^8.2.0 shared_preferences: ^2.2.3 - studyu_core: ^4.4.2 - studyu_flutter_common: ^1.8.3 - supabase: ^2.1.2 - supabase_flutter: ^2.5.2 + studyu_core: ^4.4.3-dev.1 + studyu_flutter_common: ^1.8.4-dev.1 + supabase: ^2.2.1 + supabase_flutter: ^2.5.5 timeline_tile: ^2.0.0 timezone: ^0.9.3 universal_html: ^2.2.4 - url_launcher: ^6.2.6 + url_launcher: ^6.3.0 uuid: ^4.4.0 wakelock_plus: ^1.2.5 @@ -54,6 +55,7 @@ dev_dependencies: flutter_lints: ^4.0.0 flutter_test: sdk: flutter + lint: ^2.3.0 dependency_overrides: intl: ^0.19.0 diff --git a/app/web/index.html b/app/web/index.html index f2135681c..57d3c5787 100644 --- a/app/web/index.html +++ b/app/web/index.html @@ -34,29 +34,8 @@ StudyU App | StudyU Health - - - - - + diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 7b8d97130..7ab7608e7 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -1,3 +1,13 @@ +## 4.4.3-dev.1 + + - **PERF**: improve dashboard study fetching. + - **FIX**: formatting issues. + - **FIX**: upgrade deps. + +## 4.4.3-dev.0 + + - **FIX**: upgrade deps. + ## 4.4.2 - Graduate package to a stable release. See pre-releases prior to this version for changelog entries. diff --git a/core/analysis_options.yaml b/core/analysis_options.yaml index 5e2133eb6..f689b7b48 100644 --- a/core/analysis_options.yaml +++ b/core/analysis_options.yaml @@ -1 +1,7 @@ include: ../analysis_options.yaml + +analyzer: + errors: + avoid_classes_with_only_static_members: ignore + plugins: + - custom_lint diff --git a/core/build.yaml b/core/build.yaml index 31fe4a55c..c30e6f373 100644 --- a/core/build.yaml +++ b/core/build.yaml @@ -4,5 +4,4 @@ targets: json_serializable: options: explicit_to_json: true - field_rename: none - include_if_null: false \ No newline at end of file + include_if_null: false diff --git a/core/lib/src/models/consent/consent_item.dart b/core/lib/src/models/consent/consent_item.dart index 1a340e531..82a22a1d4 100644 --- a/core/lib/src/models/consent/consent_item.dart +++ b/core/lib/src/models/consent/consent_item.dart @@ -14,7 +14,8 @@ class ConsentItem { ConsentItem.withId() : id = const Uuid().v4(); - factory ConsentItem.fromJson(Map json) => _$ConsentItemFromJson(json); + factory ConsentItem.fromJson(Map json) => + _$ConsentItemFromJson(json); Map toJson() => _$ConsentItemToJson(this); @override diff --git a/core/lib/src/models/contact.dart b/core/lib/src/models/contact.dart index 63ac7d4ad..b4013c718 100644 --- a/core/lib/src/models/contact.dart +++ b/core/lib/src/models/contact.dart @@ -15,7 +15,8 @@ class Contact { Contact(); - factory Contact.fromJson(Map json) => _$ContactFromJson(json); + factory Contact.fromJson(Map json) => + _$ContactFromJson(json); Map toJson() => _$ContactToJson(this); diff --git a/core/lib/src/models/data/data_reference.dart b/core/lib/src/models/data/data_reference.dart index ffc83e9c9..eded38996 100644 --- a/core/lib/src/models/data/data_reference.dart +++ b/core/lib/src/models/data/data_reference.dart @@ -12,7 +12,8 @@ class DataReference { DataReference(this.task, this.property); - factory DataReference.fromJson(Map json) => _$DataReferenceFromJson(json); + factory DataReference.fromJson(Map json) => + _$DataReferenceFromJson(json); Map toJson() => _$DataReferenceToJson(this); @@ -20,9 +21,14 @@ class DataReference { String toString() => toJson().toString(); Map retrieveFromResults(StudySubject subject) { - final Task? sourceTask = subject.study.observations.firstWhereOrNull((task) => task.id == this.task) ?? - subject.selectedInterventions.expand((i) => i.tasks).firstWhereOrNull((task) => task.id == this.task); - if (sourceTask == null) throw ArgumentError("Could not find a task with the id '$task'."); + final Task? sourceTask = subject.study.observations + .firstWhereOrNull((task) => task.id == this.task) ?? + subject.selectedInterventions + .expand((i) => i.tasks) + .firstWhereOrNull((task) => task.id == this.task); + if (sourceTask == null) { + throw ArgumentError("Could not find a task with the id '$task'."); + } final List sourceResults = subject.resultsFor(task); diff --git a/core/lib/src/models/eligibility/eligibility_criterion.dart b/core/lib/src/models/eligibility/eligibility_criterion.dart index af22a56b2..783f724bb 100644 --- a/core/lib/src/models/eligibility/eligibility_criterion.dart +++ b/core/lib/src/models/eligibility/eligibility_criterion.dart @@ -18,7 +18,8 @@ class EligibilityCriterion { : id = const Uuid().v4(), condition = BooleanExpression(); - factory EligibilityCriterion.fromJson(Map data) => _$EligibilityCriterionFromJson(data); + factory EligibilityCriterion.fromJson(Map data) => + _$EligibilityCriterionFromJson(data); Map toJson() => _$EligibilityCriterionToJson(this); bool isSatisfied(QuestionnaireState qs) => condition.evaluate(qs) == true; diff --git a/core/lib/src/models/expressions/expression.dart b/core/lib/src/models/expressions/expression.dart index 48690f935..52625cd0d 100644 --- a/core/lib/src/models/expressions/expression.dart +++ b/core/lib/src/models/expressions/expression.dart @@ -10,7 +10,8 @@ abstract class Expression { Expression(this.type); - factory Expression.fromJson(Map data) => switch (data[keyType]) { + factory Expression.fromJson(Map data) => + switch (data[keyType]) { BooleanExpression.expressionType => BooleanExpression.fromJson(data), ChoiceExpression.expressionType => ChoiceExpression.fromJson(data), NotExpression.expressionType => NotExpression.fromJson(data), diff --git a/core/lib/src/models/expressions/types/boolean_expression.dart b/core/lib/src/models/expressions/types/boolean_expression.dart index 40ad18430..0ee205121 100644 --- a/core/lib/src/models/expressions/types/boolean_expression.dart +++ b/core/lib/src/models/expressions/types/boolean_expression.dart @@ -10,7 +10,8 @@ class BooleanExpression extends ValueExpression { BooleanExpression() : super(expressionType); - factory BooleanExpression.fromJson(Map json) => _$BooleanExpressionFromJson(json); + factory BooleanExpression.fromJson(Map json) => + _$BooleanExpressionFromJson(json); @override Map toJson() => _$BooleanExpressionToJson(this); diff --git a/core/lib/src/models/expressions/types/choice_expression.dart b/core/lib/src/models/expressions/types/choice_expression.dart index 5983dc55f..17949d604 100644 --- a/core/lib/src/models/expressions/types/choice_expression.dart +++ b/core/lib/src/models/expressions/types/choice_expression.dart @@ -13,7 +13,8 @@ class ChoiceExpression extends ValueExpression { ChoiceExpression.withId() : super(expressionType); - factory ChoiceExpression.fromJson(Map json) => _$ChoiceExpressionFromJson(json); + factory ChoiceExpression.fromJson(Map json) => + _$ChoiceExpressionFromJson(json); @override Map toJson() => _$ChoiceExpressionToJson(this); diff --git a/core/lib/src/models/expressions/types/not_expression.dart b/core/lib/src/models/expressions/types/not_expression.dart index 679ac97e4..44d9d8443 100644 --- a/core/lib/src/models/expressions/types/not_expression.dart +++ b/core/lib/src/models/expressions/types/not_expression.dart @@ -17,7 +17,8 @@ class NotExpression extends Expression { : expression = BooleanExpression(), super(expressionType); - factory NotExpression.fromJson(Map json) => _$NotExpressionFromJson(json); + factory NotExpression.fromJson(Map json) => + _$NotExpressionFromJson(json); @override Map toJson() => _$NotExpressionToJson(this); diff --git a/core/lib/src/models/expressions/types/value_expression.dart b/core/lib/src/models/expressions/types/value_expression.dart index 371efe173..67fc8e66a 100644 --- a/core/lib/src/models/expressions/types/value_expression.dart +++ b/core/lib/src/models/expressions/types/value_expression.dart @@ -11,6 +11,8 @@ abstract class ValueExpression extends Expression { @override bool? evaluate(QuestionnaireState state) { - return state.hasAnswer(target!) ? checkValue(state.getAnswer(target!)) : null; + return state.hasAnswer(target!) + ? checkValue(state.getAnswer(target!)) + : null; } } diff --git a/core/lib/src/models/interventions/intervention.dart b/core/lib/src/models/interventions/intervention.dart index f1f841c97..0adec2258 100644 --- a/core/lib/src/models/interventions/intervention.dart +++ b/core/lib/src/models/interventions/intervention.dart @@ -17,7 +17,8 @@ class Intervention { Intervention.withId() : id = const Uuid().v4(); - factory Intervention.fromJson(Map data) => _$InterventionFromJson(data); + factory Intervention.fromJson(Map data) => + _$InterventionFromJson(data); Map toJson() => _$InterventionToJson(this); diff --git a/core/lib/src/models/interventions/intervention_task.dart b/core/lib/src/models/interventions/intervention_task.dart index fb227cb4b..b637b2aaf 100644 --- a/core/lib/src/models/interventions/intervention_task.dart +++ b/core/lib/src/models/interventions/intervention_task.dart @@ -2,14 +2,17 @@ import 'package:studyu_core/src/models/interventions/tasks/checkmark_task.dart'; import 'package:studyu_core/src/models/tasks/task.dart'; import 'package:studyu_core/src/models/unknown_json_type_error.dart'; -typedef InterventionTaskParser = InterventionTask Function(Map data); +typedef InterventionTaskParser = InterventionTask Function( + Map data, +); abstract class InterventionTask extends Task { InterventionTask(super.type); InterventionTask.withId(super.type) : super.withId(); - factory InterventionTask.fromJson(Map data) => switch (data[Task.keyType]) { + factory InterventionTask.fromJson(Map data) => + switch (data[Task.keyType]) { CheckmarkTask.taskType => CheckmarkTask.fromJson(data), _ => throw UnknownJsonTypeError(data[Task.keyType]), }; diff --git a/core/lib/src/models/interventions/tasks/checkmark_task.dart b/core/lib/src/models/interventions/tasks/checkmark_task.dart index 383d408ff..df5d86fbc 100644 --- a/core/lib/src/models/interventions/tasks/checkmark_task.dart +++ b/core/lib/src/models/interventions/tasks/checkmark_task.dart @@ -13,14 +13,20 @@ class CheckmarkTask extends InterventionTask { CheckmarkTask.withId() : super.withId(taskType); - factory CheckmarkTask.fromJson(Map json) => _$CheckmarkTaskFromJson(json); + factory CheckmarkTask.fromJson(Map json) => + _$CheckmarkTaskFromJson(json); @override Map toJson() => _$CheckmarkTaskToJson(this); @override - Map extractPropertyResults(String property, List sourceResults) { - throw ArgumentError("$runtimeType does not have a property named '$property'."); + Map extractPropertyResults( + String property, + List sourceResults, + ) { + throw ArgumentError( + "$runtimeType does not have a property named '$property'.", + ); } @override diff --git a/core/lib/src/models/observations/observation.dart b/core/lib/src/models/observations/observation.dart index cb61e4440..257fbf8f5 100644 --- a/core/lib/src/models/observations/observation.dart +++ b/core/lib/src/models/observations/observation.dart @@ -9,7 +9,8 @@ abstract class Observation extends Task { Observation.withId(super.type) : super.withId(); - factory Observation.fromJson(Map data) => switch (data[Task.keyType]) { + factory Observation.fromJson(Map data) => + switch (data[Task.keyType]) { QuestionnaireTask.taskType => QuestionnaireTask.fromJson(data), _ => throw UnknownJsonTypeError(data[Task.keyType]), }; diff --git a/core/lib/src/models/observations/tasks/questionnaire_task.dart b/core/lib/src/models/observations/tasks/questionnaire_task.dart index a4cd85d5f..6a77136dd 100644 --- a/core/lib/src/models/observations/tasks/questionnaire_task.dart +++ b/core/lib/src/models/observations/tasks/questionnaire_task.dart @@ -15,25 +15,39 @@ class QuestionnaireTask extends Observation { QuestionnaireTask.withId() : super.withId(taskType); - factory QuestionnaireTask.fromJson(Map json) => _$QuestionnaireTaskFromJson(json); + factory QuestionnaireTask.fromJson(Map json) => + _$QuestionnaireTaskFromJson(json); @override Map toJson() => _$QuestionnaireTaskToJson(this); @override - Map extractPropertyResults(String property, List sourceResults) { - final Question? targetQuestion = questions.questions.firstWhereOrNull((q) => q.id == property); + Map extractPropertyResults( + String property, + List sourceResults, + ) { + final Question? targetQuestion = + questions.questions.firstWhereOrNull((q) => q.id == property); if (targetQuestion == null) { - throw ArgumentError("Questionnaire '$id' does not have a question with '$property'."); + throw ArgumentError( + "Questionnaire '$id' does not have a question with '$property'.", + ); } return Map.fromEntries( - sourceResults - .map((e) => MapEntry(e.completedAt!, (e.result as Result).result.getAnswer(property))), + sourceResults.map( + (e) => MapEntry( + e.completedAt!, + (e.result as Result) + .result + .getAnswer(property), + ), + ), ); } @override - Map getAvailableProperties() => {for (final q in questions.questions) q.id: q.getAnswerType()}; + Map getAvailableProperties() => + {for (final q in questions.questions) q.id: q.getAnswerType()}; @override String? getHumanReadablePropertyName(String property) => diff --git a/core/lib/src/models/questionnaire/answer.dart b/core/lib/src/models/questionnaire/answer.dart index 8598b6b0c..3e904ebda 100644 --- a/core/lib/src/models/questionnaire/answer.dart +++ b/core/lib/src/models/questionnaire/answer.dart @@ -20,9 +20,11 @@ class Answer { : question = question.id, timestamp = DateTime.now(); - factory Answer.parseJson(Map json) => _$AnswerFromJson(json)..response = json[keyResponse] as V; + factory Answer.parseJson(Map json) => + _$AnswerFromJson(json)..response = json[keyResponse] as V; - Map toJson() => mergeMaps(_$AnswerToJson(this), {keyResponse: response}); + Map toJson() => + mergeMaps(_$AnswerToJson(this), {keyResponse: response}); static Answer fromJson(Map data) { final dynamic value = data[keyResponse]; diff --git a/core/lib/src/models/questionnaire/question.dart b/core/lib/src/models/questionnaire/question.dart index 910fdfdb2..a35120fbd 100644 --- a/core/lib/src/models/questionnaire/question.dart +++ b/core/lib/src/models/questionnaire/question.dart @@ -21,14 +21,19 @@ abstract class Question { Question.withId(this.type) : id = const Uuid().v4(); - factory Question.fromJson(Map data) => switch (data[keyType]) { + factory Question.fromJson(Map data) => + switch (data[keyType]) { BooleanQuestion.questionType => BooleanQuestion.fromJson(data), ChoiceQuestion.questionType => ChoiceQuestion.fromJson(data), ScaleQuestion.questionType => ScaleQuestion.fromJson(data), - AnnotatedScaleQuestion.questionType => AnnotatedScaleQuestion.fromJson(data), - VisualAnalogueQuestion.questionType => VisualAnalogueQuestion.fromJson(data), - ImageCapturingQuestion.questionType => ImageCapturingQuestion.fromJson(data), - AudioRecordingQuestion.questionType => AudioRecordingQuestion.fromJson(data), + AnnotatedScaleQuestion.questionType => + AnnotatedScaleQuestion.fromJson(data), + VisualAnalogueQuestion.questionType => + VisualAnalogueQuestion.fromJson(data), + ImageCapturingQuestion.questionType => + ImageCapturingQuestion.fromJson(data), + AudioRecordingQuestion.questionType => + AudioRecordingQuestion.fromJson(data), FreeTextQuestion.questionType => FreeTextQuestion.fromJson(data), _ => throw UnknownJsonTypeError(data[keyType]), } as Question; diff --git a/core/lib/src/models/questionnaire/question_conditional.dart b/core/lib/src/models/questionnaire/question_conditional.dart index ce19e1ba9..3816daa9e 100644 --- a/core/lib/src/models/questionnaire/question_conditional.dart +++ b/core/lib/src/models/questionnaire/question_conditional.dart @@ -13,10 +13,13 @@ class QuestionConditional { QuestionConditional(); - factory QuestionConditional.fromJson(Map json) => _fromJson(json); + factory QuestionConditional.fromJson(Map json) => + _fromJson(json); static QuestionConditional _fromJson(Map json) => - _$QuestionConditionalFromJson(json)..defaultValue = json[keyDefaultValue] as V?; + _$QuestionConditionalFromJson(json) + ..defaultValue = json[keyDefaultValue] as V?; - Map toJson() => _$QuestionConditionalToJson(this)..[keyDefaultValue] = defaultValue; + Map toJson() => + _$QuestionConditionalToJson(this)..[keyDefaultValue] = defaultValue; } diff --git a/core/lib/src/models/questionnaire/questionnaire.dart b/core/lib/src/models/questionnaire/questionnaire.dart index 2dcaf98b5..a82979f43 100644 --- a/core/lib/src/models/questionnaire/questionnaire.dart +++ b/core/lib/src/models/questionnaire/questionnaire.dart @@ -7,7 +7,11 @@ class StudyUQuestionnaire { StudyUQuestionnaire(); factory StudyUQuestionnaire.fromJson(List data) => - StudyUQuestionnaire()..questions = data.map((entry) => Question.fromJson(entry as Map)).toList(); + StudyUQuestionnaire() + ..questions = data + .map((entry) => Question.fromJson(entry as Map)) + .toList(); - List toJson() => questions.map((question) => question.toJson()).toList(); + List toJson() => + questions.map((question) => question.toJson()).toList(); } diff --git a/core/lib/src/models/questionnaire/questionnaire_state.dart b/core/lib/src/models/questionnaire/questionnaire_state.dart index a6ec625ac..76aa90241 100644 --- a/core/lib/src/models/questionnaire/questionnaire_state.dart +++ b/core/lib/src/models/questionnaire/questionnaire_state.dart @@ -10,7 +10,8 @@ class QuestionnaireState { json.map(Answer.fromJson), key: (answer) => (answer as Answer).question, ); - List> toJson() => answers.values.map((answer) => answer.toJson()).toList(); + List> toJson() => + answers.values.map((answer) => answer.toJson()).toList(); bool hasAnswer(String question) { return answers[question] is Answer; @@ -21,7 +22,9 @@ class QuestionnaireState { if (answer is Answer) { return answer.response; } else { - throw ArgumentError("'Answer<$T>' requested but found '${answer.runtimeType}'."); + throw ArgumentError( + "'Answer<$T>' requested but found '${answer.runtimeType}'.", + ); } } } diff --git a/core/lib/src/models/questionnaire/questions/annotated_scale_question.dart b/core/lib/src/models/questionnaire/questions/annotated_scale_question.dart index 02cbd7f8b..ad8e1320a 100644 --- a/core/lib/src/models/questionnaire/questions/annotated_scale_question.dart +++ b/core/lib/src/models/questionnaire/questions/annotated_scale_question.dart @@ -15,7 +15,8 @@ class AnnotatedScaleQuestion extends SliderQuestion { AnnotatedScaleQuestion.withId() : super.withId(questionType); - factory AnnotatedScaleQuestion.fromJson(Map json) => _$AnnotatedScaleQuestionFromJson(json); + factory AnnotatedScaleQuestion.fromJson(Map json) => + _$AnnotatedScaleQuestionFromJson(json); @override Map toJson() => _$AnnotatedScaleQuestionToJson(this); @@ -28,7 +29,8 @@ class Annotation { Annotation(); - factory Annotation.fromJson(Map json) => _$AnnotationFromJson(json); + factory Annotation.fromJson(Map json) => + _$AnnotationFromJson(json); Map toJson() => _$AnnotationToJson(this); } diff --git a/core/lib/src/models/questionnaire/questions/audio_recording_question.dart b/core/lib/src/models/questionnaire/questions/audio_recording_question.dart index c88e41001..9d53591ba 100644 --- a/core/lib/src/models/questionnaire/questions/audio_recording_question.dart +++ b/core/lib/src/models/questionnaire/questions/audio_recording_question.dart @@ -12,13 +12,17 @@ class AudioRecordingQuestion extends Question { @JsonKey(name: 'maxRecordingDurationSeconds') final int maxRecordingDurationSeconds; - AudioRecordingQuestion({required this.maxRecordingDurationSeconds}) : super(questionType); + AudioRecordingQuestion({required this.maxRecordingDurationSeconds}) + : super(questionType); - AudioRecordingQuestion.withId(this.maxRecordingDurationSeconds) : super.withId(questionType); + AudioRecordingQuestion.withId(this.maxRecordingDurationSeconds) + : super.withId(questionType); - factory AudioRecordingQuestion.fromJson(Map json) => _$AudioRecordingQuestionFromJson(json); + factory AudioRecordingQuestion.fromJson(Map json) => + _$AudioRecordingQuestionFromJson(json); @override Map toJson() => _$AudioRecordingQuestionToJson(this); - Answer constructAnswer(FutureBlobFile response) => Answer.forQuestion(this, response); + Answer constructAnswer(FutureBlobFile response) => + Answer.forQuestion(this, response); } diff --git a/core/lib/src/models/questionnaire/questions/boolean_question.dart b/core/lib/src/models/questionnaire/questions/boolean_question.dart index b7eea0389..ca81b6843 100644 --- a/core/lib/src/models/questionnaire/questions/boolean_question.dart +++ b/core/lib/src/models/questionnaire/questions/boolean_question.dart @@ -14,10 +14,12 @@ class BooleanQuestion extends Question { BooleanQuestion.withId() : super.withId(questionType); - factory BooleanQuestion.fromJson(Map json) => _$BooleanQuestionFromJson(json); + factory BooleanQuestion.fromJson(Map json) => + _$BooleanQuestionFromJson(json); @override Map toJson() => _$BooleanQuestionToJson(this); // ignore: avoid_positional_boolean_parameters - Answer constructAnswer(bool response) => Answer.forQuestion(this, response); + Answer constructAnswer(bool response) => + Answer.forQuestion(this, response); } diff --git a/core/lib/src/models/questionnaire/questions/choice_question.dart b/core/lib/src/models/questionnaire/questions/choice_question.dart index c24ddf1e6..ac0271b90 100644 --- a/core/lib/src/models/questionnaire/questions/choice_question.dart +++ b/core/lib/src/models/questionnaire/questions/choice_question.dart @@ -17,7 +17,8 @@ class ChoiceQuestion extends Question> { ChoiceQuestion.withId() : super.withId(questionType); - factory ChoiceQuestion.fromJson(Map json) => _$ChoiceQuestionFromJson(json); + factory ChoiceQuestion.fromJson(Map json) => + _$ChoiceQuestionFromJson(json); @override Map toJson() => _$ChoiceQuestionToJson(this); diff --git a/core/lib/src/models/questionnaire/questions/free_text_question.dart b/core/lib/src/models/questionnaire/questions/free_text_question.dart index e1971b8ad..c3b8a351e 100644 --- a/core/lib/src/models/questionnaire/questions/free_text_question.dart +++ b/core/lib/src/models/questionnaire/questions/free_text_question.dart @@ -19,17 +19,25 @@ class FreeTextQuestion extends Question { @JsonKey(name: 'customTypeExpression') String? customTypeExpression; - FreeTextQuestion({required this.textType, required this.lengthRange, this.customTypeExpression}) - : super(questionType); - - FreeTextQuestion.withId({required this.textType, required this.lengthRange, this.customTypeExpression}) - : super.withId(questionType); - - factory FreeTextQuestion.fromJson(Map json) => _$FreeTextQuestionFromJson(json); + FreeTextQuestion({ + required this.textType, + required this.lengthRange, + this.customTypeExpression, + }) : super(questionType); + + FreeTextQuestion.withId({ + required this.textType, + required this.lengthRange, + this.customTypeExpression, + }) : super.withId(questionType); + + factory FreeTextQuestion.fromJson(Map json) => + _$FreeTextQuestionFromJson(json); @override Map toJson() => _$FreeTextQuestionToJson(this); - Answer constructAnswer(String response) => Answer.forQuestion(this, response); + Answer constructAnswer(String response) => + Answer.forQuestion(this, response); } enum FreeTextQuestionType { diff --git a/core/lib/src/models/questionnaire/questions/image_capturing_question.dart b/core/lib/src/models/questionnaire/questions/image_capturing_question.dart index fa5b06359..f527a76cb 100644 --- a/core/lib/src/models/questionnaire/questions/image_capturing_question.dart +++ b/core/lib/src/models/questionnaire/questions/image_capturing_question.dart @@ -13,9 +13,11 @@ class ImageCapturingQuestion extends Question { ImageCapturingQuestion.withId() : super.withId(questionType); - factory ImageCapturingQuestion.fromJson(Map json) => _$ImageCapturingQuestionFromJson(json); + factory ImageCapturingQuestion.fromJson(Map json) => + _$ImageCapturingQuestionFromJson(json); @override Map toJson() => _$ImageCapturingQuestionToJson(this); - Answer constructAnswer(FutureBlobFile response) => Answer.forQuestion(this, response); + Answer constructAnswer(FutureBlobFile response) => + Answer.forQuestion(this, response); } diff --git a/core/lib/src/models/questionnaire/questions/scale_question.dart b/core/lib/src/models/questionnaire/questions/scale_question.dart index 0345f968e..23912cf7b 100644 --- a/core/lib/src/models/questionnaire/questions/scale_question.dart +++ b/core/lib/src/models/questionnaire/questions/scale_question.dart @@ -9,7 +9,8 @@ import 'package:studyu_core/src/models/questionnaire/question_conditional.dart'; part 'scale_question.g.dart'; @JsonSerializable() -class ScaleQuestion extends SliderQuestion implements AnnotatedScaleQuestion, VisualAnalogueQuestion { +class ScaleQuestion extends SliderQuestion + implements AnnotatedScaleQuestion, VisualAnalogueQuestion { static const String questionType = 'scale'; @override @@ -40,7 +41,8 @@ class ScaleQuestion extends SliderQuestion implements AnnotatedScaleQuestion, Vi ScaleQuestion.withId() : super.withId(questionType); - factory ScaleQuestion.fromJson(Map json) => _$ScaleQuestionFromJson(json); + factory ScaleQuestion.fromJson(Map json) => + _$ScaleQuestionFromJson(json); factory ScaleQuestion.fromAnnotatedScaleQuestion( AnnotatedScaleQuestion question, @@ -81,7 +83,8 @@ class ScaleQuestion extends SliderQuestion implements AnnotatedScaleQuestion, Vi Map toJson() => _$ScaleQuestionToJson(this); @JsonKey(includeToJson: false, includeFromJson: false) - List get annotationsSorted => annotations.sorted((a, b) => a.value.compareTo(b.value)); + List get annotationsSorted => + annotations.sorted((a, b) => a.value.compareTo(b.value)); @JsonKey(includeToJson: false, includeFromJson: false) Annotation? get minAnnotation { @@ -128,8 +131,9 @@ class ScaleQuestion extends SliderQuestion implements AnnotatedScaleQuestion, Vi } @JsonKey(includeToJson: false, includeFromJson: false) - List get midAnnotations => - annotationsSorted.where((a) => a.value != minimum && a.value != maximum).toList(); + List get midAnnotations => annotationsSorted + .where((a) => a.value != minimum && a.value != maximum) + .toList(); set midAnnotations(List annotations) { final prevMinAnnotation = minAnnotation; @@ -148,10 +152,12 @@ class ScaleQuestion extends SliderQuestion implements AnnotatedScaleQuestion, Vi } @JsonKey(includeToJson: false, includeFromJson: false) - List get midLabels => midAnnotations.map((a) => a.annotation).toList(); + List get midLabels => + midAnnotations.map((a) => a.annotation).toList(); @JsonKey(includeToJson: false, includeFromJson: false) - List get midValues => midAnnotations.map((a) => a.value.toDouble()).toList(); + List get midValues => + midAnnotations.map((a) => a.value.toDouble()).toList(); Annotation addAnnotation({required int value, required String label}) { final annotation = Annotation() @@ -191,11 +197,13 @@ class ScaleQuestion extends SliderQuestion implements AnnotatedScaleQuestion, Vi @override @JsonKey(includeToJson: false, includeFromJson: false) - String get minimumAnnotation => minAnnotation?.annotation ?? minimum.toString(); + String get minimumAnnotation => + minAnnotation?.annotation ?? minimum.toString(); @override @JsonKey(includeToJson: false, includeFromJson: false) - String get maximumAnnotation => maxAnnotation?.annotation ?? maximum.toString(); + String get maximumAnnotation => + maxAnnotation?.annotation ?? maximum.toString(); @override @JsonKey(includeToJson: false, includeFromJson: false) @@ -243,10 +251,15 @@ class ScaleQuestion extends SliderQuestion implements AnnotatedScaleQuestion, Vi required int scaleMaxValue, int numValuesGenerated = 10, }) { - final int midValueStepSize = getAutostepSize(scaleMaxValue: scaleMinValue, numValuesGenerated: numValuesGenerated); + final int midValueStepSize = getAutostepSize( + scaleMaxValue: scaleMinValue, + numValuesGenerated: numValuesGenerated, + ); final List midValues = []; - for (int midValue = scaleMinValue + midValueStepSize; midValue < scaleMaxValue; midValue += midValueStepSize) { + for (int midValue = scaleMinValue + midValueStepSize; + midValue < scaleMaxValue; + midValue += midValueStepSize) { midValues.add(midValue); if (midValues.length >= numValuesGenerated) { break; diff --git a/core/lib/src/models/questionnaire/questions/slider_question.dart b/core/lib/src/models/questionnaire/questions/slider_question.dart index b3bc6ece8..714381220 100644 --- a/core/lib/src/models/questionnaire/questions/slider_question.dart +++ b/core/lib/src/models/questionnaire/questions/slider_question.dart @@ -19,5 +19,6 @@ abstract class SliderQuestion extends Question { SliderQuestion.withId(super.type) : super.withId(); - Answer constructAnswer(double response) => Answer.forQuestion(this, response); + Answer constructAnswer(double response) => + Answer.forQuestion(this, response); } diff --git a/core/lib/src/models/questionnaire/questions/visual_analogue_question.dart b/core/lib/src/models/questionnaire/questions/visual_analogue_question.dart index 7cce9fa46..a36fb5de0 100644 --- a/core/lib/src/models/questionnaire/questions/visual_analogue_question.dart +++ b/core/lib/src/models/questionnaire/questions/visual_analogue_question.dart @@ -19,7 +19,8 @@ class VisualAnalogueQuestion extends SliderQuestion { VisualAnalogueQuestion.withId() : super.withId(questionType); - factory VisualAnalogueQuestion.fromJson(Map json) => _$VisualAnalogueQuestionFromJson(json); + factory VisualAnalogueQuestion.fromJson(Map json) => + _$VisualAnalogueQuestionFromJson(json); @override Map toJson() => _$VisualAnalogueQuestionToJson(this); diff --git a/core/lib/src/models/report/report_section.dart b/core/lib/src/models/report/report_section.dart index b6b0b25ac..fb9bfa082 100644 --- a/core/lib/src/models/report/report_section.dart +++ b/core/lib/src/models/report/report_section.dart @@ -16,9 +16,11 @@ abstract class ReportSection { ReportSection.withId(this.type) : id = const Uuid().v4(); - factory ReportSection.fromJson(Map data) => switch (data[keyType]) { + factory ReportSection.fromJson(Map data) => + switch (data[keyType]) { AverageSection.sectionType => AverageSection.fromJson(data), - LinearRegressionSection.sectionType => LinearRegressionSection.fromJson(data), + LinearRegressionSection.sectionType => + LinearRegressionSection.fromJson(data), _ => throw UnknownJsonTypeError(data[keyType]), }; Map toJson(); diff --git a/core/lib/src/models/report/report_specification.dart b/core/lib/src/models/report/report_specification.dart index 23a89392b..1d6a3b899 100644 --- a/core/lib/src/models/report/report_specification.dart +++ b/core/lib/src/models/report/report_specification.dart @@ -11,7 +11,8 @@ class ReportSpecification { ReportSpecification(); - factory ReportSpecification.fromJson(Map json) => _$ReportSpecificationFromJson(json); + factory ReportSpecification.fromJson(Map json) => + _$ReportSpecificationFromJson(json); Map toJson() => _$ReportSpecificationToJson(this); @override diff --git a/core/lib/src/models/report/sections/average_section.dart b/core/lib/src/models/report/sections/average_section.dart index 6e1ebdf91..5cbebb81b 100644 --- a/core/lib/src/models/report/sections/average_section.dart +++ b/core/lib/src/models/report/sections/average_section.dart @@ -17,7 +17,8 @@ class AverageSection extends ReportSection { AverageSection.withId() : super.withId(sectionType); - factory AverageSection.fromJson(Map json) => _$AverageSectionFromJson(json); + factory AverageSection.fromJson(Map json) => + _$AverageSectionFromJson(json); @override Map toJson() => _$AverageSectionToJson(this); } diff --git a/core/lib/src/models/report/sections/linear_regression_section.dart b/core/lib/src/models/report/sections/linear_regression_section.dart index 2e984402d..0a837a1d8 100644 --- a/core/lib/src/models/report/sections/linear_regression_section.dart +++ b/core/lib/src/models/report/sections/linear_regression_section.dart @@ -19,7 +19,8 @@ class LinearRegressionSection extends ReportSection { LinearRegressionSection.withId() : super.withId(sectionType); - factory LinearRegressionSection.fromJson(Map json) => _$LinearRegressionSectionFromJson(json); + factory LinearRegressionSection.fromJson(Map json) => + _$LinearRegressionSectionFromJson(json); @override Map toJson() => _$LinearRegressionSectionToJson(this); diff --git a/core/lib/src/models/results/result.dart b/core/lib/src/models/results/result.dart index fcac02dda..885248d3e 100644 --- a/core/lib/src/models/results/result.dart +++ b/core/lib/src/models/results/result.dart @@ -19,20 +19,29 @@ class Result { Result(this.type); - Result.app({required this.type, required this.periodId, required this.result}); + Result.app({ + required this.type, + required this.periodId, + required this.result, + }); factory Result.parseJson(Map json) => _$ResultFromJson(json); factory Result.fromJson(Map json) => switch (json[keyType]) { 'QuestionnaireState' => Result.parseJson(json) - ..result = QuestionnaireState.fromJson(List>.from(json[keyResult] as List)), - 'bool' => Result.parseJson(json)..result = json[keyResult] as bool, + ..result = QuestionnaireState.fromJson( + List>.from(json[keyResult] as List), + ), + 'bool' => Result.parseJson(json) + ..result = json[keyResult] as bool, _ => throw UnknownJsonTypeError(json[keyType]), } as Result; Map toJson() { final Map resultMap = switch (type) { - 'QuestionnaireState' => {keyResult: (result as QuestionnaireState).toJson()}, + 'QuestionnaireState' => { + keyResult: (result as QuestionnaireState).toJson(), + }, 'bool' => {keyResult: result}, _ => throw ArgumentError('Unknown result type $type'), }; diff --git a/core/lib/src/models/study_results/results/intervention_result.dart b/core/lib/src/models/study_results/results/intervention_result.dart index be8c6c13c..36b946831 100644 --- a/core/lib/src/models/study_results/results/intervention_result.dart +++ b/core/lib/src/models/study_results/results/intervention_result.dart @@ -13,7 +13,8 @@ class InterventionResult extends StudyResult { InterventionResult.withId() : super.withId(studyResultType); - factory InterventionResult.fromJson(Map json) => _$InterventionResultFromJson(json); + factory InterventionResult.fromJson(Map json) => + _$InterventionResultFromJson(json); @override Map toJson() => _$InterventionResultToJson(this); @@ -22,13 +23,20 @@ class InterventionResult extends StudyResult { List getHeaders(Study studySpec) { final schedule = studySpec.schedule; final numberOfDays = schedule.getNumberOfPhases() * schedule.phaseDuration; - return Iterable.generate(numberOfDays).map((e) => e.toString()).toList(); + return Iterable.generate(numberOfDays) + .map((e) => e.toString()) + .toList(); } @override List getValues(StudySubject subject) { return subject.interventionOrder - .expand((intervention) => List.filled(subject.study.schedule.phaseDuration, intervention)) + .expand( + (intervention) => List.filled( + subject.study.schedule.phaseDuration, + intervention, + ), + ) .toList(); } } diff --git a/core/lib/src/models/study_results/results/numeric_result.dart b/core/lib/src/models/study_results/results/numeric_result.dart index 14182359a..8529e998a 100644 --- a/core/lib/src/models/study_results/results/numeric_result.dart +++ b/core/lib/src/models/study_results/results/numeric_result.dart @@ -16,7 +16,8 @@ class NumericResult extends StudyResult { NumericResult.withId() : super.withId(studyResultType); - factory NumericResult.fromJson(Map json) => _$NumericResultFromJson(json); + factory NumericResult.fromJson(Map json) => + _$NumericResultFromJson(json); @override Map toJson() => _$NumericResultToJson(this); @@ -25,15 +26,20 @@ class NumericResult extends StudyResult { List getHeaders(Study studySpec) { final schedule = studySpec.schedule; final numberOfDays = schedule.getNumberOfPhases() * schedule.phaseDuration; - return Iterable.generate(numberOfDays).map((e) => e.toString()).toList(); + return Iterable.generate(numberOfDays) + .map((e) => e.toString()) + .toList(); } @override List getValues(StudySubject subject) { - final resultSet = resultProperty - .retrieveFromResults(subject) - .map((key, value) => MapEntry(subject.getDayOfStudyFor(key), value)); - final numberOfDays = subject.study.schedule.getNumberOfPhases() * subject.study.schedule.phaseDuration; - return Iterable.generate(numberOfDays).map((day) => resultSet[day]).toList(); + final resultSet = resultProperty.retrieveFromResults(subject).map( + (key, value) => MapEntry(subject.getDayOfStudyFor(key), value), + ); + final numberOfDays = subject.study.schedule.getNumberOfPhases() * + subject.study.schedule.phaseDuration; + return Iterable.generate(numberOfDays) + .map((day) => resultSet[day]) + .toList(); } } diff --git a/core/lib/src/models/study_results/study_result.dart b/core/lib/src/models/study_results/study_result.dart index 80f8ee1ac..966b16f94 100644 --- a/core/lib/src/models/study_results/study_result.dart +++ b/core/lib/src/models/study_results/study_result.dart @@ -17,7 +17,8 @@ abstract class StudyResult { StudyResult.withId(this.type) : id = const Uuid().v4(); - factory StudyResult.fromJson(Map data) => switch (data[keyType]) { + factory StudyResult.fromJson(Map data) => + switch (data[keyType]) { InterventionResult.studyResultType => InterventionResult.fromJson(data), NumericResult.studyResultType => NumericResult.fromJson(data), _ => throw UnknownJsonTypeError(data[keyType]), diff --git a/core/lib/src/models/study_schedule/study_schedule.dart b/core/lib/src/models/study_schedule/study_schedule.dart index 420c989b4..85ae581b0 100644 --- a/core/lib/src/models/study_schedule/study_schedule.dart +++ b/core/lib/src/models/study_schedule/study_schedule.dart @@ -17,16 +17,20 @@ class StudySchedule { this.sequenceCustom = 'ABAB', }); - factory StudySchedule.fromJson(Map json) => _$StudyScheduleFromJson(json); + factory StudySchedule.fromJson(Map json) => + _$StudyScheduleFromJson(json); Map toJson() => _$StudyScheduleToJson(this); - int getNumberOfPhases() => numberOfCycles * numberOfInterventions + (includeBaseline ? 1 : 0); + int getNumberOfPhases() => + numberOfCycles * numberOfInterventions + (includeBaseline ? 1 : 0); int get length => getNumberOfPhases() * phaseDuration; List generateWith(int firstIntervention) { final cycles = Iterable.generate(numberOfCycles); - final phases = cycles.expand((cycle) => _generateCycle(firstIntervention, cycle)).toList(); + final phases = cycles + .expand((cycle) => _generateCycle(firstIntervention, cycle)) + .toList(); return phases; } @@ -54,7 +58,8 @@ class StudySchedule { } } - List _generateAlternatingCycle(int first, int cycle) => [first, _nextIntervention(first)]; + List _generateAlternatingCycle(int first, int cycle) => + [first, _nextIntervention(first)]; List _generateCounterBalancedCycle(int first, int cycle) { final shift = ((cycle + 1) ~/ 2) % 2; diff --git a/core/lib/src/models/tables/app_config.dart b/core/lib/src/models/tables/app_config.dart index 020097540..36a3fe17d 100644 --- a/core/lib/src/models/tables/app_config.dart +++ b/core/lib/src/models/tables/app_config.dart @@ -38,7 +38,8 @@ class AppConfig extends SupabaseObjectFunctions { required this.analytics, }); - factory AppConfig.fromJson(Map json) => _$AppConfigFromJson(json); + factory AppConfig.fromJson(Map json) => + _$AppConfigFromJson(json); @override Map toJson() => _$AppConfigToJson(this); diff --git a/core/lib/src/models/tables/repo.dart b/core/lib/src/models/tables/repo.dart index d681fbfe6..830825d94 100644 --- a/core/lib/src/models/tables/repo.dart +++ b/core/lib/src/models/tables/repo.dart @@ -26,7 +26,14 @@ class Repo extends SupabaseObjectFunctions { @JsonKey(name: 'git_url') String? gitUrl; - Repo(this.projectId, this.userId, this.studyId, this.provider, this.webUrl, this.gitUrl); + Repo( + this.projectId, + this.userId, + this.studyId, + this.provider, + this.webUrl, + this.gitUrl, + ); factory Repo.fromJson(Map json) => _$RepoFromJson(json); diff --git a/core/lib/src/models/tables/study.dart b/core/lib/src/models/tables/study.dart index a168737c4..9a5106c7c 100644 --- a/core/lib/src/models/tables/study.dart +++ b/core/lib/src/models/tables/study.dart @@ -35,7 +35,8 @@ enum ResultSharing { } @JsonSerializable() -class Study extends SupabaseObjectFunctions implements Comparable { +class Study extends SupabaseObjectFunctions + implements Comparable { static const String tableName = 'study'; @override @@ -50,23 +51,31 @@ class Study extends SupabaseObjectFunctions implements Comparable Participation participation = Participation.invite; @JsonKey(name: 'result_sharing') ResultSharing resultSharing = ResultSharing.private; + @JsonKey(fromJson: _contactFromJson) late Contact contact = Contact(); - @JsonKey(name: 'icon_name') + @JsonKey(name: 'icon_name', defaultValue: 'accountHeart') late String iconName = 'accountHeart'; + @JsonKey(defaultValue: false) late bool published = false; + @JsonKey(fromJson: _questionnaireFromJson) late StudyUQuestionnaire questionnaire = StudyUQuestionnaire(); - @JsonKey(name: 'eligibility_criteria') + @JsonKey(name: 'eligibility_criteria', fromJson: _eligibilityCriteriaFromJson) late List eligibilityCriteria = []; + @JsonKey(defaultValue: []) late List consent = []; + @JsonKey(defaultValue: []) late List interventions = []; + @JsonKey(defaultValue: []) late List observations = []; + @JsonKey(fromJson: _studyScheduleFromJson) late StudySchedule schedule = StudySchedule(); - @JsonKey(name: 'report_specification') + @JsonKey(name: 'report_specification', fromJson: _reportSpecificationFromJson) late ReportSpecification reportSpecification = ReportSpecification(); + @JsonKey(defaultValue: []) late List results = []; - @JsonKey(name: 'collaborator_emails') + @JsonKey(name: 'collaborator_emails', defaultValue: []) late List collaboratorEmails = []; - @JsonKey(name: 'registry_published') + @JsonKey(name: 'registry_published', defaultValue: false) late bool registryPublished = false; @JsonKey(includeToJson: false, includeFromJson: false) @@ -97,30 +106,73 @@ class Study extends SupabaseObjectFunctions implements Comparable Study.withId(this.userId) : id = const Uuid().v4(); + static List _eligibilityCriteriaFromJson(dynamic json) { + if (json == null) { + return []; + } + return (json as List) + .map((e) => EligibilityCriterion.fromJson(e as Map)) + .toList(); + } + + static Contact _contactFromJson(dynamic json) { + if (json is Map) { + return Contact.fromJson(json); + } + return Contact(); + } + + static StudySchedule _studyScheduleFromJson(dynamic json) { + if (json is Map) { + return StudySchedule.fromJson(json); + } + return StudySchedule(); + } + + static StudyUQuestionnaire _questionnaireFromJson(dynamic json) { + if (json is List) { + return StudyUQuestionnaire.fromJson(json); + } + return StudyUQuestionnaire(); + } + + static ReportSpecification _reportSpecificationFromJson(dynamic json) { + if (json is Map) { + return ReportSpecification.fromJson(json); + } + return ReportSpecification(); + } + factory Study.fromJson(Map json) { final study = _$StudyFromJson(json); final List? repo = json['repo'] as List?; if (repo != null && repo.isNotEmpty) { - study.repo = Repo.fromJson((json['repo'] as List)[0] as Map); + study.repo = + Repo.fromJson((json['repo'] as List)[0] as Map); } final List? invites = json['study_invite'] as List?; if (invites != null) { - study.invites = invites.map((json) => StudyInvite.fromJson(json as Map)).toList(); + study.invites = invites + .map((json) => StudyInvite.fromJson(json as Map)) + .toList(); } final List? participants = json['study_subject'] as List?; if (participants != null) { - study.participants = participants.map((json) => StudySubject.fromJson(json as Map)).toList(); + study.participants = participants + .map((json) => StudySubject.fromJson(json as Map)) + .toList(); } List? participantsProgress = json['study_progress'] as List?; participantsProgress = json['study_progress_export'] as List?; participantsProgress ??= json['subject_progress'] as List?; if (participantsProgress != null) { - study.participantsProgress = - participantsProgress.map((json) => SubjectProgress.fromJson(json as Map)).toList(); + study.participantsProgress = participantsProgress + .map((json) => SubjectProgress.fromJson(json as Map)) + .toList(); } final int? participantCount = json['study_participant_count'] as int?; @@ -155,7 +207,8 @@ class Study extends SupabaseObjectFunctions implements Comparable Map toJson() => _$StudyToJson(this); // TODO: Add null checks in fromJson to allow selecting columns - static Future> getResearcherDashboardStudies() async => SupabaseQuery.getAll( + static Future> getResearcherDashboardStudies() async => + SupabaseQuery.getAll( selectedColumns: [ '*', 'repo(*)', @@ -166,12 +219,31 @@ class Study extends SupabaseObjectFunctions implements Comparable ], ); + /*static Future> getDashboardDisplayStudies() async => SupabaseQuery.getAll( + selectedColumns: [ + 'id', + 'title', + 'description', + 'user_id', + 'participation', + 'result_sharing', + 'published', + 'registry_published', + 'study_participant_count', + 'study_ended_count', + 'active_subject_count', + ], + );*/ + // ['id', 'title', 'description', 'published', 'icon_name', 'results', 'schedule'] static Future> publishedPublicStudies() async { ExtractionResult result; try { - final response = await env.client.from(tableName).select().eq('participation', 'open'); - final extracted = SupabaseQuery.extractSupabaseList(List>.from(response)); + final response = + await env.client.from(tableName).select().eq('participation', 'open'); + final extracted = SupabaseQuery.extractSupabaseList( + List>.from(response), + ); result = ExtractionSuccess(extracted); } on ExtractionFailedException catch (error) { result = error; @@ -184,22 +256,30 @@ class Study extends SupabaseObjectFunctions implements Comparable bool isOwner(User? user) => user != null && userId == user.id; - bool isEditor(User? user) => user != null && collaboratorEmails.contains(user.email); + bool isEditor(User? user) => + user != null && collaboratorEmails.contains(user.email); bool canEdit(User? user) => user != null && (isOwner(user) || isEditor(user)); - bool get hasEligibilityCheck => eligibilityCriteria.isNotEmpty && questionnaire.questions.isNotEmpty; + bool get hasEligibilityCheck => + eligibilityCriteria.isNotEmpty && questionnaire.questions.isNotEmpty; bool get hasConsentCheck => consent.isNotEmpty; - int get totalMissedDays => missedDays.isNotEmpty ? missedDays.reduce((total, days) => total += days) : 0; + int get totalMissedDays => missedDays.isNotEmpty + ? missedDays.reduce((total, days) => total += days) + : 0; - double get percentageMissedDays => totalMissedDays / (participantCount * schedule.length); + double get percentageMissedDays => + totalMissedDays / (participantCount * schedule.length); static Future fetchResultsCSVTable(String studyId) async { final List res; try { - res = await env.client.from('study_progress').select().eq('study_id', studyId); + res = await env.client + .from('study_progress') + .select() + .eq('study_id', studyId); } catch (error, stacktrace) { SupabaseQuery.catchSupabaseException(error, stacktrace); rethrow; @@ -210,7 +290,8 @@ class Study extends SupabaseObjectFunctions implements Comparable final tableHeadersSet = jsonList[0].keys.toSet(); final flattenedQuestions = jsonList.map((progress) { if (progress['result_type'] == 'QuestionnaireState') { - for (final result in List>.from(progress['result'] as List)) { + for (final result + in List>.from(progress['result'] as List)) { progress[result['question'] as String] = result['response']; tableHeadersSet.add(result['question'] as String); } @@ -223,7 +304,9 @@ class Study extends SupabaseObjectFunctions implements Comparable final resultsTable = [ tableHeaders, ...flattenedQuestions.map( - (progress) => tableHeaders.map((header) => progress[header] ?? '').toList(growable: false), + (progress) => tableHeaders + .map((header) => progress[header] ?? '') + .toList(growable: false), ), ]; return const ListToCsvConverter().convert(resultsTable); diff --git a/core/lib/src/models/tables/study.g.dart b/core/lib/src/models/tables/study.g.dart index bc1917f4d..e1b6daece 100644 --- a/core/lib/src/models/tables/study.g.dart +++ b/core/lib/src/models/tables/study.g.dart @@ -16,34 +16,36 @@ Study _$StudyFromJson(Map json) => Study( $enumDecode(_$ParticipationEnumMap, json['participation']) ..resultSharing = $enumDecode(_$ResultSharingEnumMap, json['result_sharing']) - ..contact = Contact.fromJson(json['contact'] as Map) - ..iconName = json['icon_name'] as String - ..published = json['published'] as bool - ..questionnaire = - StudyUQuestionnaire.fromJson(json['questionnaire'] as List) - ..eligibilityCriteria = (json['eligibility_criteria'] as List) - .map((e) => EligibilityCriterion.fromJson(e as Map)) - .toList() - ..consent = (json['consent'] as List) - .map((e) => ConsentItem.fromJson(e as Map)) - .toList() - ..interventions = (json['interventions'] as List) - .map((e) => Intervention.fromJson(e as Map)) - .toList() - ..observations = (json['observations'] as List) - .map((e) => Observation.fromJson(e as Map)) - .toList() - ..schedule = - StudySchedule.fromJson(json['schedule'] as Map) - ..reportSpecification = ReportSpecification.fromJson( - json['report_specification'] as Map) - ..results = (json['results'] as List) - .map((e) => StudyResult.fromJson(e as Map)) - .toList() - ..collaboratorEmails = (json['collaborator_emails'] as List) - .map((e) => e as String) - .toList() - ..registryPublished = json['registry_published'] as bool; + ..contact = Study._contactFromJson(json['contact']) + ..iconName = json['icon_name'] as String? ?? 'accountHeart' + ..published = json['published'] as bool? ?? false + ..questionnaire = Study._questionnaireFromJson(json['questionnaire']) + ..eligibilityCriteria = + Study._eligibilityCriteriaFromJson(json['eligibility_criteria']) + ..consent = (json['consent'] as List?) + ?.map((e) => ConsentItem.fromJson(e as Map)) + .toList() ?? + [] + ..interventions = (json['interventions'] as List?) + ?.map((e) => Intervention.fromJson(e as Map)) + .toList() ?? + [] + ..observations = (json['observations'] as List?) + ?.map((e) => Observation.fromJson(e as Map)) + .toList() ?? + [] + ..schedule = Study._studyScheduleFromJson(json['schedule']) + ..reportSpecification = + Study._reportSpecificationFromJson(json['report_specification']) + ..results = (json['results'] as List?) + ?.map((e) => StudyResult.fromJson(e as Map)) + .toList() ?? + [] + ..collaboratorEmails = (json['collaborator_emails'] as List?) + ?.map((e) => e as String) + .toList() ?? + [] + ..registryPublished = json['registry_published'] as bool? ?? false; Map _$StudyToJson(Study instance) { final val = { diff --git a/core/lib/src/models/tables/study_invite.dart b/core/lib/src/models/tables/study_invite.dart index fbf91bd38..bd47e93a2 100644 --- a/core/lib/src/models/tables/study_invite.dart +++ b/core/lib/src/models/tables/study_invite.dart @@ -20,7 +20,8 @@ class StudyInvite extends SupabaseObjectFunctions { StudyInvite(this.code, this.studyId, {this.preselectedInterventionIds}); - factory StudyInvite.fromJson(Map json) => _$StudyInviteFromJson(json); + factory StudyInvite.fromJson(Map json) => + _$StudyInviteFromJson(json); @override Map toJson() => _$StudyInviteToJson(this); diff --git a/core/lib/src/models/tables/study_subject.dart b/core/lib/src/models/tables/study_subject.dart index ff95169d4..36130409a 100644 --- a/core/lib/src/models/tables/study_subject.dart +++ b/core/lib/src/models/tables/study_subject.dart @@ -39,7 +39,12 @@ class StudySubject extends SupabaseObjectFunctions { @JsonKey(includeToJson: false, includeFromJson: false) late List progress = []; - StudySubject(this.id, this.studyId, this.userId, this.selectedInterventionIds); + StudySubject( + this.id, + this.studyId, + this.userId, + this.selectedInterventionIds, + ); factory StudySubject.fromJson(Map json) { final subject = _$StudySubjectFromJson(json); @@ -51,7 +56,9 @@ class StudySubject extends SupabaseObjectFunctions { final List? progress = json['subject_progress'] as List?; if (progress != null) { - subject.progress = progress.map((json) => SubjectProgress.fromJson(json as Map)).toList(); + subject.progress = progress + .map((json) => SubjectProgress.fromJson(json as Map)) + .toList(); } return subject; @@ -60,17 +67,31 @@ class StudySubject extends SupabaseObjectFunctions { @override Map toJson() => _$StudySubjectToJson(this); - StudySubject.fromStudy(this.study, this.userId, this.selectedInterventionIds, this.inviteCode) - : id = const Uuid().v4(), + StudySubject.fromStudy( + this.study, + this.userId, + this.selectedInterventionIds, + this.inviteCode, + ) : id = const Uuid().v4(), studyId = study.id; +<<<<<<< HEAD List get interventionOrder => study.schedule.generateInterventionIdsInOrder(selectedInterventionIds); +======= + List get interventionOrder => [ + if (study.schedule.includeBaseline) Study.baselineID, + ...study.schedule + .generateWith(0) + .map((int index) => selectedInterventionIds[index]), + ]; +>>>>>>> dev List get selectedInterventions { final selectedInterventions = selectedInterventionIds .map( - (selectedInterventionId) => - study.interventions.singleWhere((intervention) => intervention.id == selectedInterventionId), + (selectedInterventionId) => study.interventions.singleWhere( + (intervention) => intervention.id == selectedInterventionId, + ), ) .toList(); if (study.schedule.includeBaseline) { @@ -83,12 +104,19 @@ class StudySubject extends SupabaseObjectFunctions { return selectedInterventions; } - int get daysPerIntervention => study.schedule.numberOfCycles * study.schedule.phaseDuration; + int get daysPerIntervention => + study.schedule.numberOfCycles * study.schedule.phaseDuration; - Map> getResultsByDate({required String interventionId}) { + Map> getResultsByDate({ + required String interventionId, + }) { final resultsByDate = >{}; progress.where((p) => p.interventionId == interventionId).forEach((p) { - final date = DateTime(p.completedAt!.year, p.completedAt!.month, p.completedAt!.day); + final date = DateTime( + p.completedAt!.year, + p.completedAt!.month, + p.completedAt!.day, + ); resultsByDate.putIfAbsent(date, () => []); resultsByDate[date]!.add(p); }); @@ -96,7 +124,9 @@ class StudySubject extends SupabaseObjectFunctions { } // Day after last intervention - DateTime endDate(DateTime dt) => dt.add(Duration(days: interventionOrder.length * study.schedule.phaseDuration)); + DateTime endDate(DateTime dt) => dt.add( + Duration(days: interventionOrder.length * study.schedule.phaseDuration), + ); int getDayOfStudyFor(DateTime date) { return date.differenceInDays(startedAt!); @@ -114,20 +144,27 @@ class StudySubject extends SupabaseObjectFunctions { return null; } final interventionId = interventionOrder[index]; - return selectedInterventions.firstWhereOrNull((intervention) => intervention.id == interventionId); + return selectedInterventions + .firstWhereOrNull((intervention) => intervention.id == interventionId); } List getInterventionsInOrder() { return interventionOrder - .map((key) => selectedInterventions.firstWhere((intervention) => intervention.id == key)) + .map( + (key) => selectedInterventions + .firstWhere((intervention) => intervention.id == key), + ) .toList(); } - DateTime startOfPhase(int index) => startedAt!.add(Duration(days: study.schedule.phaseDuration * index)); + DateTime startOfPhase(int index) => + startedAt!.add(Duration(days: study.schedule.phaseDuration * index)); - DateTime dayAfterEndOfPhase(int index) => startOfPhase(index).add(Duration(days: study.schedule.phaseDuration)); + DateTime dayAfterEndOfPhase(int index) => + startOfPhase(index).add(Duration(days: study.schedule.phaseDuration)); - List resultsFor(String taskId) => progress.where((p) => p.taskId == taskId).toList(); + List resultsFor(String taskId) => + progress.where((p) => p.taskId == taskId).toList(); int completedForPhase(int index) { final start = startOfPhase(index); @@ -142,7 +179,8 @@ class StudySubject extends SupabaseObjectFunctions { int daysLeftForPhase(int index) { final start = startOfPhase(index); - return study.schedule.phaseDuration - DateTime.now().differenceInDays(start); + return study.schedule.phaseDuration - + DateTime.now().differenceInDays(start); } double percentCompletedForPhase(int index) { @@ -152,12 +190,18 @@ class StudySubject extends SupabaseObjectFunctions { double percentMissedForPhase(int index, DateTime date) { if (startOfPhase(index).isAfter(date)) return 0; - final missedInPhase = - min(date.differenceInDays(startOfPhase(index)), study.schedule.phaseDuration) - completedForPhase(index); + final missedInPhase = min( + date.differenceInDays(startOfPhase(index)), + study.schedule.phaseDuration, + ) - + completedForPhase(index); return missedInPhase / study.schedule.phaseDuration; } - List getTaskProgressForDay(String taskId, DateTime dateTime) { + List getTaskProgressForDay( + String taskId, + DateTime dateTime, + ) { final List thisTaskProgressToday = []; for (final SubjectProgress sp in resultsFor(taskId)) { if (sp.subjectId == id && sp.completedAt!.isSameDate(dateTime)) { @@ -170,7 +214,11 @@ class StudySubject extends SupabaseObjectFunctions { /// Check if a task instance is completed /// returns true if a given task has been completed for a specific /// completionPeriod on a given day - bool completedTaskInstanceForDay(String taskId, CompletionPeriod completionPeriod, DateTime dateTime) { + bool completedTaskInstanceForDay( + String taskId, + CompletionPeriod completionPeriod, + DateTime dateTime, + ) { return getTaskProgressForDay(taskId, dateTime).any( (progress) { if (progress.result.periodId == null) { @@ -186,12 +234,10 @@ class StudySubject extends SupabaseObjectFunctions { /// returns true if a given task has been completed for all of its /// completionPeriods on a given day bool completedTaskForDay(String taskId, DateTime dateTime) { - return [...selectedInterventions.expand((e) => e.tasks), ...study.observations] - .where((task) => task.id == taskId) - .single - .schedule - .completionPeriods - .any( + return [ + ...selectedInterventions.expand((e) => e.tasks), + ...study.observations, + ].where((task) => task.id == taskId).single.schedule.completionPeriods.any( (period) => completedTaskInstanceForDay(taskId, period, dateTime), ); } @@ -218,7 +264,8 @@ class StudySubject extends SupabaseObjectFunctions { int totalTaskCountFor(Task task) { var daysCount = daysPerIntervention; if (task is Observation) { - daysCount = 2 * daysCount + (study.schedule.includeBaseline ? study.schedule.phaseDuration : 0); + daysCount = 2 * daysCount + + (study.schedule.includeBaseline ? study.schedule.phaseDuration : 0); } return daysCount * task.schedule.completionPeriods.length; } @@ -254,8 +301,11 @@ class StudySubject extends SupabaseObjectFunctions { @override Future save() async { try { - final response = await env.client.from(tableName).upsert(toJson()).select(); - final json = toFullJson(partialJson: List>.from(response).single); + final response = + await env.client.from(tableName).upsert(toJson()).select(); + final json = toFullJson( + partialJson: List>.from(response).single, + ); final newSubject = StudySubject.fromJson(json); _controller.add(newSubject); // print("Saving study subject"); @@ -277,7 +327,10 @@ class StudySubject extends SupabaseObjectFunctions { Future deleteProgress() async { try { - await env.client.from(SubjectProgress.tableName).delete().eq('subject_id', id); + await env.client + .from(SubjectProgress.tableName) + .delete() + .eq('subject_id', id); } catch (error, stacktrace) { SupabaseQuery.catchSupabaseException(error, stacktrace); rethrow; @@ -289,7 +342,9 @@ class StudySubject extends SupabaseObjectFunctions { .map((p) => (p.result.result as QuestionnaireState).answers.values) .expand((answers) => answers) .where( - (e) => e.question == AudioRecordingQuestion.questionType || e.question == ImageCapturingQuestion.questionType, + (e) => + e.question == AudioRecordingQuestion.questionType || + e.question == ImageCapturingQuestion.questionType, ) .map((e) => e.response!.toString()) .toList(); @@ -303,7 +358,12 @@ class StudySubject extends SupabaseObjectFunctions { Future delete() async { await deleteProgress(); try { - final response = await env.client.from(tableName).delete().eq('id', id).select().single(); + final response = await env.client + .from(tableName) + .delete() + .eq('id', id) + .select() + .single(); response['study'] = study.toJson(); return StudySubject.fromJson(response); } catch (error, stacktrace) { @@ -319,7 +379,9 @@ class StudySubject extends SupabaseObjectFunctions { static Future> getStudyHistory(String userId) async { return SupabaseQuery.extractSupabaseList( - await env.client.from(tableName).select('*,study!study_subject_studyId_fkey(*),subject_progress(*)'), + await env.client + .from(tableName) + .select('*,study!study_subject_studyId_fkey(*),subject_progress(*)'), ); } diff --git a/core/lib/src/models/tables/subject_progress.dart b/core/lib/src/models/tables/subject_progress.dart index 8ec968c4f..8f2e6211b 100644 --- a/core/lib/src/models/tables/subject_progress.dart +++ b/core/lib/src/models/tables/subject_progress.dart @@ -11,7 +11,8 @@ class SubjectProgress extends SupabaseObjectFunctions { static const String tableName = 'subject_progress'; @override - Map get primaryKeys => {'completed_at': completedAt!, 'subject_id': subjectId}; + Map get primaryKeys => + {'completed_at': completedAt!, 'subject_id': subjectId}; // stored in UTC format, be careful when comparing dates @JsonKey(name: 'completed_at') diff --git a/core/lib/src/models/tables/user.dart b/core/lib/src/models/tables/user.dart index 08a35866c..00510cc2e 100644 --- a/core/lib/src/models/tables/user.dart +++ b/core/lib/src/models/tables/user.dart @@ -21,7 +21,8 @@ class StudyUUser extends SupabaseObjectFunctions { StudyUUser({required this.id, required this.email, Preferences? preferences}) : preferences = preferences ?? Preferences(); - factory StudyUUser.fromJson(Map json) => _$StudyUUserFromJson(json); + factory StudyUUser.fromJson(Map json) => + _$StudyUUserFromJson(json); @override Map toJson() => _$StudyUUserToJson(this); @@ -38,13 +39,17 @@ class Preferences { Preferences({this.language = '', this.pinnedStudies = const {}}); - factory Preferences.fromJson(Map json) => _$PreferencesFromJson(json); + factory Preferences.fromJson(Map json) => + _$PreferencesFromJson(json); Map toJson() { final Map json = _$PreferencesToJson(this); // Remove empty fields from the JSON map json.removeWhere( - (key, value) => value == null || value is String && value.isEmpty || value is Set && value.isEmpty, + (key, value) => + value == null || + value is String && value.isEmpty || + value is Set && value.isEmpty, ); return json; } diff --git a/core/lib/src/models/tasks/schedule.dart b/core/lib/src/models/tasks/schedule.dart index b5aae370b..b8ca80110 100644 --- a/core/lib/src/models/tasks/schedule.dart +++ b/core/lib/src/models/tasks/schedule.dart @@ -17,7 +17,8 @@ class Schedule { Schedule(); - factory Schedule.fromJson(Map json) => _$ScheduleFromJson(json); + factory Schedule.fromJson(Map json) => + _$ScheduleFromJson(json); Map toJson() => _$ScheduleToJson(this); @@ -33,11 +34,17 @@ class CompletionPeriod { final StudyUTimeOfDay unlockTime; final StudyUTimeOfDay lockTime; - CompletionPeriod({required this.id, required this.unlockTime, required this.lockTime}); + CompletionPeriod({ + required this.id, + required this.unlockTime, + required this.lockTime, + }); - CompletionPeriod.noId({required this.unlockTime, required this.lockTime}) : id = const Uuid().v4(); + CompletionPeriod.noId({required this.unlockTime, required this.lockTime}) + : id = const Uuid().v4(); - factory CompletionPeriod.fromJson(Map json) => _$CompletionPeriodFromJson(json); + factory CompletionPeriod.fromJson(Map json) => + _$CompletionPeriodFromJson(json); Map toJson() => _$CompletionPeriodToJson(this); diff --git a/core/lib/src/models/tasks/task.dart b/core/lib/src/models/tasks/task.dart index cb796d149..1ef7dc641 100644 --- a/core/lib/src/models/tasks/task.dart +++ b/core/lib/src/models/tasks/task.dart @@ -24,7 +24,10 @@ abstract class Task { return toJson().toString(); } - Map extractPropertyResults(String property, List sourceResults); + Map extractPropertyResults( + String property, + List sourceResults, + ); Map getAvailableProperties(); diff --git a/core/lib/src/models/tasks/task_instance.dart b/core/lib/src/models/tasks/task_instance.dart index 921a4d72a..b57a59821 100644 --- a/core/lib/src/models/tasks/task_instance.dart +++ b/core/lib/src/models/tasks/task_instance.dart @@ -6,7 +6,12 @@ class TaskInstance { TaskInstance(this.task, this.id) : assert(task.id != id); - factory TaskInstance.fromInstanceId(String taskInstanceId, {StudySubject? subject, Study? study, DateTime? date}) { + factory TaskInstance.fromInstanceId( + String taskInstanceId, { + StudySubject? subject, + Study? study, + DateTime? date, + }) { date ??= DateTime.now(); final Task tempTask; if (subject != null) { @@ -20,22 +25,37 @@ class TaskInstance { return TaskInstance(tempTask, taskInstanceId); } - static Task _taskFromStudy(String taskInstanceId, Study study, DateTime date) { + static Task _taskFromStudy( + String taskInstanceId, + Study study, + DateTime date, + ) { final tasks = [ ...study.observations, - ...study.interventions.map((intervention) => intervention.tasks).expand((element) => element), + ...study.interventions + .map((intervention) => intervention.tasks) + .expand((element) => element), ]; return tasks.firstWhere((task) { - if (task.schedule.completionPeriods.any((completionPeriod) => completionPeriod.id == taskInstanceId)) { + if (task.schedule.completionPeriods + .any((completionPeriod) => completionPeriod.id == taskInstanceId)) { return true; } return false; }); } - static Task _taskFromSubject(String taskInstanceId, StudySubject subject, DateTime now) { - return subject.scheduleFor(now).firstWhere((element) => element.id == taskInstanceId).task; + static Task _taskFromSubject( + String taskInstanceId, + StudySubject subject, + DateTime now, + ) { + return subject + .scheduleFor(now) + .firstWhere((element) => element.id == taskInstanceId) + .task; } - CompletionPeriod get completionPeriod => task.schedule.completionPeriods.firstWhere((element) => element.id == id); + CompletionPeriod get completionPeriod => + task.schedule.completionPeriods.firstWhere((element) => element.id == id); } diff --git a/core/lib/src/util/analytics.dart b/core/lib/src/util/analytics.dart index 35f2b511e..b02d60489 100644 --- a/core/lib/src/util/analytics.dart +++ b/core/lib/src/util/analytics.dart @@ -39,7 +39,10 @@ class StudyUDiagnostics { await Sentry.captureMessage(message); } - static void addBreadcrumb({required String message, required String category}) { + static void addBreadcrumb({ + required String message, + required String category, + }) { print("[Breadcrumb] $category: $message"); Sentry.addBreadcrumb(Breadcrumb(message: message, category: category)); } @@ -118,7 +121,8 @@ class StudyUAnalytics { StudyUAnalytics(this.enabled, this.dsn, this.samplingRate); - factory StudyUAnalytics.fromJson(Map json) => _$StudyUAnalyticsFromJson(json); + factory StudyUAnalytics.fromJson(Map json) => + _$StudyUAnalyticsFromJson(json); Map toJson() => _$StudyUAnalyticsToJson(this); } diff --git a/core/lib/src/util/extensions.dart b/core/lib/src/util/extensions.dart index 05418facd..1f862fe91 100644 --- a/core/lib/src/util/extensions.dart +++ b/core/lib/src/util/extensions.dart @@ -1,7 +1,9 @@ extension DateOnlyCompare on DateTime { bool isSameDate(DateTime other) { final otherUtc = other.toUtc(); - return toUtc().year == otherUtc.year && toUtc().month == otherUtc.month && toUtc().day == otherUtc.day; + return toUtc().year == otherUtc.year && + toUtc().month == otherUtc.month && + toUtc().day == otherUtc.day; } bool isEarlierDateThan(DateTime other) { @@ -21,7 +23,8 @@ extension DateOnlyCompare on DateTime { } bool isLaterDateThan(DateTime other) { - return !(toUtc().isSameDate(other.toUtc()) || toUtc().isEarlierDateThan(other.toUtc())); + return !(toUtc().isSameDate(other.toUtc()) || + toUtc().isEarlierDateThan(other.toUtc())); } int differenceInDays(DateTime other) { diff --git a/core/lib/src/util/multimodal/blob_storage_handler.dart b/core/lib/src/util/multimodal/blob_storage_handler.dart index 083f98602..33de4eea9 100644 --- a/core/lib/src/util/multimodal/blob_storage_handler.dart +++ b/core/lib/src/util/multimodal/blob_storage_handler.dart @@ -8,14 +8,20 @@ class BlobStorageHandler { static const String _observationsBucketName = 'observations'; Future uploadObservation(String blobPath, File file) async { - await env.client.storage.from(_observationsBucketName).upload(blobPath, file); + await env.client.storage + .from(_observationsBucketName) + .upload(blobPath, file); } Future downloadObservation(String blobPath) async { - return await env.client.storage.from(_observationsBucketName).download(blobPath); + return await env.client.storage + .from(_observationsBucketName) + .download(blobPath); } Future> removeObservation(List blobPaths) async { - return await env.client.storage.from(_observationsBucketName).remove(blobPaths); + return await env.client.storage + .from(_observationsBucketName) + .remove(blobPaths); } } diff --git a/core/lib/src/util/supabase_object.dart b/core/lib/src/util/supabase_object.dart index b204076db..fbe2b3894 100644 --- a/core/lib/src/util/supabase_object.dart +++ b/core/lib/src/util/supabase_object.dart @@ -21,8 +21,10 @@ String tableName(Type cls) => switch (cls) { _ => throw ArgumentError('$cls is not a supported Supabase type'), }; -abstract class SupabaseObjectFunctions implements SupabaseObject { - static T fromJson(Map json) => switch (T) { +abstract class SupabaseObjectFunctions + implements SupabaseObject { + static T fromJson(Map json) => + switch (T) { == Study => Study.fromJson(json) as T, == StudySubject => StudySubject.fromJson(json) as T, == SubjectProgress => SubjectProgress.fromJson(json) as T, @@ -34,12 +36,18 @@ abstract class SupabaseObjectFunctions implements Supa }; Future delete() async => SupabaseQuery.extractSupabaseSingleRow( - await env.client.from(tableName(T)).delete().primaryKeys(primaryKeys).select().single(), + await env.client + .from(tableName(T)) + .delete() + .primaryKeys(primaryKeys) + .select() + .single(), ); Future save() async { - return SupabaseQuery.extractSupabaseList(await env.client.from(tableName(T)).upsert(this.toJson()).select()) - .single; + return SupabaseQuery.extractSupabaseList( + await env.client.from(tableName(T)).upsert(this.toJson()).select(), + ).single; } } @@ -49,17 +57,26 @@ class SupabaseQuery { List selectedColumns = const ['*'], }) async { try { - return extractSupabaseList(await env.client.from(tableName(T)).select(selectedColumns.join(','))); + return extractSupabaseList( + await env.client.from(tableName(T)).select(selectedColumns.join(',')), + ); } catch (error, stacktrace) { catchSupabaseException(error, stacktrace); rethrow; } } - static Future getById(String id, {List selectedColumns = const ['*']}) async { + static Future getById( + String id, { + List selectedColumns = const ['*'], + }) async { try { return extractSupabaseSingleRow( - await env.client.from(tableName(T)).select(selectedColumns.join(',')).eq('id', id).single(), + await env.client + .from(tableName(T)) + .select(selectedColumns.join(',')) + .eq('id', id) + .single(), ); } catch (error, stacktrace) { catchSupabaseException(error, stacktrace); @@ -71,7 +88,9 @@ class SupabaseQuery { List> batchJson, ) async { try { - return SupabaseQuery.extractSupabaseList(await env.client.from(tableName(T)).upsert(batchJson).select()); + return SupabaseQuery.extractSupabaseList( + await env.client.from(tableName(T)).upsert(batchJson).select(), + ); } catch (error, stacktrace) { catchSupabaseException(error, stacktrace); rethrow; @@ -104,14 +123,18 @@ class SupabaseQuery { return extracted; } - static T extractSupabaseSingleRow(Map response) { + static T extractSupabaseSingleRow( + Map response, + ) { return SupabaseObjectFunctions.fromJson(response); } static void catchSupabaseException(Object error, StackTrace stacktrace) { StudyUDiagnostics.captureException(error, stackTrace: stacktrace); if (error is PostgrestException) { - StudyULogger.fatal('Caught Postgrest Error: $error\nStacktrace: $stacktrace'); + StudyULogger.fatal( + 'Caught Postgrest Error: $error\nStacktrace: $stacktrace', + ); throw error; } else if (error is SocketException) { // StudyULogger.info("App is suspected to be offline"); @@ -143,7 +166,8 @@ class ExtractionSuccess extends ExtractionResult { ExtractionSuccess(super.extracted); } -class ExtractionFailedException extends ExtractionResult implements Exception { +class ExtractionFailedException extends ExtractionResult + implements Exception { final List notExtracted; ExtractionFailedException(super.extracted, this.notExtracted); diff --git a/core/pubspec.lock b/core/pubspec.lock index e5bb2e1a2..890bbe4e0 100644 --- a/core/pubspec.lock +++ b/core/pubspec.lock @@ -5,18 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: "5aaf60d96c4cd00fe7f21594b5ad6a1b699c80a27420f8a837f4d68473ef09e3" url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "68.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.1.0" analyzer: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: "21f1d3720fd1c70316399d5e2bccaebb415c434592d778cce8acb967b8578808" url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.5.0" args: dependency: transitive description: @@ -61,10 +66,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" build_resolvers: dependency: transitive description: @@ -77,18 +82,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.11" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.3.1" built_collection: dependency: transitive description: @@ -141,10 +146,10 @@ packages: dependency: transitive description: name: coverage - sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" + sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" url: "https://pub.dev" source: hosted - version: "1.7.2" + version: "1.8.0" crypto: dependency: transitive description: @@ -197,10 +202,10 @@ packages: dependency: transitive description: name: functions_client - sha256: a70b0dd9a1c35d05d1141557f7e49ffe4de5f450ffde31755a9eeeadca03b8ee + sha256: "48659e5c6a4bbe02659102bf6406a0cf39142202deae65aacfa78688f2e68946" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" glob: dependency: transitive description: @@ -213,10 +218,10 @@ packages: dependency: transitive description: name: gotrue - sha256: c9c984f088320a5c5e87c7a34571e3de3982cca4cbd8b978e59d36baf748edfb + sha256: b7324a9113bb21c354fd52531d9a9dba6570a9c37b2fc6b424865feb19974476 url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.8.0" graphs: dependency: transitive description: @@ -313,6 +318,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + macros: + dependency: transitive + description: + name: macros + sha256: "12e8a9842b5a7390de7a781ec63d793527582398d16ea26c60fed58833c9ae79" + url: "https://pub.dev" + source: hosted + version: "0.1.0-main.0" matcher: dependency: transitive description: @@ -325,10 +338,10 @@ packages: dependency: transitive description: name: meta - sha256: "25dfcaf170a0190f47ca6355bdd4552cb8924b430512ff0cafb8db9bd41fe33b" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.14.0" + version: "1.15.0" mime: dependency: transitive description: @@ -373,10 +386,10 @@ packages: dependency: transitive description: name: postgrest - sha256: "9a3b590cf123f8d323b6a918702e037f037027d12a01902f9dc6ee38fdc05d6c" + sha256: f1f78470a74c611811132ff12acdef9c08b3ec65b61e88161a057d6cc5fbbd83 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" pub_semver: dependency: transitive description: @@ -389,10 +402,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" quiver: dependency: "direct main" description: @@ -405,10 +418,10 @@ packages: dependency: transitive description: name: realtime_client - sha256: "492a1ab568b3812cb345aad8dd09b3936877edba81a6ab6f5fdf365c155797e1" + sha256: cd44fa21407a2e217d674f1c1a33b36c49ad0d8aea0349bf5b66594db06c80fb url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.1.0" retry: dependency: transitive description: @@ -429,10 +442,10 @@ packages: dependency: "direct main" description: name: sentry - sha256: "1d2952d40b99da0dc4bf3ba4797e3985dd60cc61a13d0a1d2c62b02f6528441a" + sha256: fd1fbfe860c05f5c52820ec4dbf2b6473789e83ead26cfc18bca4fe80bf3f008 url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.2.0" shelf: dependency: transitive description: @@ -461,10 +474,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" source_gen: dependency: transitive description: @@ -525,10 +538,10 @@ packages: dependency: transitive description: name: storage_client - sha256: bf5589d5de61a2451edb1b8960a0e673d4bb5c42ecc4dddf7c051a93789ced34 + sha256: e37f1b9d40f43078d12bd2d1b6b08c2c16fbdbafc58b57bc44922da6ea3f5625 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" stream_channel: dependency: transitive description: @@ -557,10 +570,10 @@ packages: dependency: "direct main" description: name: supabase - sha256: ef407187b18c440f4a5c3f3cf30eb5cc1daadd4ff5616febf445a37e0e0ed34e + sha256: "1133466d2ec9441e7cff6bf73943f0a6e17cbb5044f6327ca93e3dddc567c882" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.2.1" term_glyph: dependency: transitive description: @@ -573,26 +586,26 @@ packages: dependency: "direct dev" description: name: test - sha256: d11b55850c68c1f6c0cf00eabded4e66c4043feaf6c0d7ce4a36785137df6331 + sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" url: "https://pub.dev" source: hosted - version: "1.25.5" + version: "1.25.7" test_api: dependency: transitive description: name: test_api - sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.7.2" test_core: dependency: transitive description: name: test_core - sha256: "4d070a6bc36c1c4e89f20d353bfd71dc30cdf2bd0e14349090af360a029ab292" + sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" url: "https://pub.dev" source: hosted - version: "0.6.2" + version: "0.6.4" timing: dependency: transitive description: @@ -621,10 +634,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "360c4271613beb44db559547d02f8b0dc044741d0eeb9aa6ccdb47e8ec54c63a" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.3" watcher: dependency: transitive description: @@ -674,4 +687,4 @@ packages: source: hosted version: "2.0.0" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.4.0 <4.0.0" diff --git a/core/pubspec.yaml b/core/pubspec.yaml index c5361ac52..4ce8cb6b3 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -1,5 +1,5 @@ name: studyu_core -version: 4.4.2 +version: 4.4.3-dev.1 description: This package contains StudyU models and common functions for the app and designer packages homepage: https://studyu.health repository: https://github.com/hpi-studyu/studyu @@ -13,12 +13,12 @@ dependencies: json_annotation: ^4.9.0 logger: ^2.3.0 quiver: ^3.2.1 - sentry: ^8.1.0 - supabase: ^2.1.2 + sentry: ^8.2.0 + supabase: ^2.2.1 uuid: ^4.4.0 dev_dependencies: - build_runner: ^2.4.9 + build_runner: ^2.4.11 json_serializable: ^6.8.0 lint: ^2.3.0 - test: ^1.25.5 + test: ^1.25.7 diff --git a/core/test/model/study_schedule/study_schedule_test.dart b/core/test/model/study_schedule/study_schedule_test.dart index 3f4ed1ee8..da8638139 100644 --- a/core/test/model/study_schedule/study_schedule_test.dart +++ b/core/test/model/study_schedule/study_schedule_test.dart @@ -15,8 +15,16 @@ void main() { final result = schedule.generateWith(0); - expect(result.sublist(0).take(2).toSet().length, 2, reason: 'A cycle was not complete'); - expect(result.sublist(2).take(2).toSet().length, 2, reason: 'A cycle was not complete'); + expect( + result.sublist(0).take(2).toSet().length, + 2, + reason: 'A cycle was not complete', + ); + expect( + result.sublist(2).take(2).toSet().length, + 2, + reason: 'A cycle was not complete', + ); }); test('respects the first intervention', () { @@ -25,8 +33,16 @@ void main() { ..phaseDuration = 7 ..includeBaseline = false; - expect(schedule.generateWith(0).first, 0, reason: 'Did not respect first intervention'); - expect(schedule.generateWith(1).first, 1, reason: 'Did not respect first intervention'); + expect( + schedule.generateWith(0).first, + 0, + reason: 'Did not respect first intervention', + ); + expect( + schedule.generateWith(1).first, + 1, + reason: 'Did not respect first intervention', + ); }); } @@ -43,7 +59,11 @@ void main() { void checkAlternating(int first) { final result = schedule.generateWith(first); for (var i = 0; i < result.length - 1; i++) { - expect(result[i], isNot(equals(result[i + 1])), reason: 'Phase $i and ${i + 1} have the same intervention'); + expect( + result[i], + isNot(equals(result[i + 1])), + reason: 'Phase $i and ${i + 1} have the same intervention', + ); } } diff --git a/designer_v2/CHANGELOG.md b/designer_v2/CHANGELOG.md index dbe71f863..290a2e136 100644 --- a/designer_v2/CHANGELOG.md +++ b/designer_v2/CHANGELOG.md @@ -1,3 +1,26 @@ +## 1.8.1-dev.2 + + - **PERF**: added comments to the getUserStudies function. + - **PERF**: improve dashboard study fetching. + - **FIX**: add emojis again. + - **FIX**: integration test sign out. + - **FIX**: check if canPop. + - **FIX**: upgrade deps. + - **FIX**: add compatibility for emoji font with flutter >= 3.22. + - **FIX**: Flutter 3.22 arg error. + +## 1.8.1-dev.1 + + - **FIX**: add emojis again. + +## 1.8.1-dev.0 + + - **FIX**: integration test sign out. + - **FIX**: check if canPop. + - **FIX**: upgrade deps. + - **FIX**: add compatibility for emoji font with flutter >= 3.22. + - **FIX**: Flutter 3.22 arg error. + ## 1.8.0 - Graduate package to a stable release. See pre-releases prior to this version for changelog entries. diff --git a/designer_v2/analysis_options.yaml b/designer_v2/analysis_options.yaml index 3d0cfc1ee..76e535897 100644 --- a/designer_v2/analysis_options.yaml +++ b/designer_v2/analysis_options.yaml @@ -1,5 +1,8 @@ -include: ../flutter_analysis_options.yaml +include: ../analysis_options.yaml analyzer: + errors: + avoid_classes_with_only_static_members: ignore + unsafe_html: ignore plugins: - custom_lint diff --git a/designer_v2/integration_test/app_test.dart b/designer_v2/integration_test/app_test.dart index 32c695770..1e55e29a2 100644 --- a/designer_v2/integration_test/app_test.dart +++ b/designer_v2/integration_test/app_test.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:patrol_finders/patrol_finders.dart'; +import 'package:flutter_web_plugins/url_strategy.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:patrol_finders/patrol_finders.dart'; import 'package:studyu_designer_v2/features/app.dart'; -import 'package:studyu_flutter_common/studyu_flutter_common.dart'; import 'package:studyu_designer_v2/utils/performance.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_web_plugins/url_strategy.dart'; +import 'package:studyu_flutter_common/studyu_flutter_common.dart'; import 'robots/robots.dart'; @@ -120,7 +120,7 @@ void main() { */ // We start with a new account and study here, since tests run asynchronously and // previous tests might not have been completed when running this one - patrolWidgetTest('Publish a study', semanticsEnabled: false, ($) async { + patrolWidgetTest('Publish a study', (PatrolTester $) async { final appRobot = AppRobot($); // RM final authRobot = AuthRobot($); // RM final studiesRobot = StudiesRobot($); @@ -175,9 +175,11 @@ void main() { await studyInfoRobot.validateOnStudyInfoScreen(); await studyInfoRobot.enterStudyDescription('Test study description'); await studyInfoRobot.enterResponsibleOrg('Test Organization, Inc.'); - await studyInfoRobot.enterInstitutionalReviewBoard('IRB of Test Organization, Inc.'); + await studyInfoRobot + .enterInstitutionalReviewBoard('IRB of Test Organization, Inc.'); await studyInfoRobot.enterIRBProtocolNumber('456-112-324'); - await studyInfoRobot.enterResponsiblePerson('Test First Name, Test Last Name'); + await studyInfoRobot + .enterResponsiblePerson('Test First Name, Test Last Name'); await studyInfoRobot.enterWebsite('test-study.org'); await studyInfoRobot.enterContactEmail('test@email.com'); await studyInfoRobot.enterContactPhone('+491112221122'); @@ -189,21 +191,27 @@ void main() { await studyDesignRobot.navigateToInterventionsScreen(); // Repeat twice for two interventions await studyInterventionsRobot.tapAddInterventionButton(); - await studyInterventionsRobot.enterInterventionName('Test Intervention A'); - await studyInterventionsRobot.enterInterventionDesciption('Test Intervention Description A'); + await studyInterventionsRobot + .enterInterventionName('Test Intervention A'); + await studyInterventionsRobot + .enterInterventionDesciption('Test Intervention Description A'); await studyInterventionsRobot.tapAddInterventionTaskButton(); await studyInterventionsRobot.enterInterventionTaskName('Task 1A'); - await studyInterventionsRobot.enterInterventionTaskDescription('Task 1A Description'); + await studyInterventionsRobot + .enterInterventionTaskDescription('Task 1A Description'); await studyInterventionsRobot.tapSaveInterventionTaskButton(); await studyInterventionsRobot.tapSaveInterventionButton(); await studyDesignRobot.validateChangesSaved(); await studyInterventionsRobot.tapAddInterventionButton(); - await studyInterventionsRobot.enterInterventionName('Test Intervention B'); - await studyInterventionsRobot.enterInterventionDesciption('Test Intervention Description B'); + await studyInterventionsRobot + .enterInterventionName('Test Intervention B'); + await studyInterventionsRobot + .enterInterventionDesciption('Test Intervention Description B'); await studyInterventionsRobot.tapAddInterventionTaskButton(); await studyInterventionsRobot.enterInterventionTaskName('Task 1B'); - await studyInterventionsRobot.enterInterventionTaskDescription('Task 1B Description'); + await studyInterventionsRobot + .enterInterventionTaskDescription('Task 1B Description'); await studyInterventionsRobot.tapSaveInterventionTaskButton(); await studyInterventionsRobot.tapSaveInterventionButton(); await studyDesignRobot.validateChangesSaved(); @@ -231,7 +239,7 @@ void main() { await studyDesignRobot.tapMyStudiesButton(); await studiesRobot.validateOnStudiesScreen(); - // await studiesRobot.tapSignOutButton(); // does not work + await studiesRobot.tapSignOutButton(); // todo dump database data and validate its state }); diff --git a/designer_v2/integration_test/robots/auth_robot.dart b/designer_v2/integration_test/robots/auth_robot.dart index 35e2be55a..80cf87e76 100644 --- a/designer_v2/integration_test/robots/auth_robot.dart +++ b/designer_v2/integration_test/robots/auth_robot.dart @@ -13,11 +13,15 @@ class AuthRobot { } Future enterPassword(String password) async { - await $(ReactiveTextField).containing(tr.form_field_password).enterText(password); + await $(ReactiveTextField) + .containing(tr.form_field_password) + .enterText(password); } Future enterPasswordConfirmation(String confirmPassword) async { - await $(ReactiveTextField).containing(tr.form_field_password_confirm).enterText(confirmPassword); + await $(ReactiveTextField) + .containing(tr.form_field_password_confirm) + .enterText(confirmPassword); } Future tapTermsCheckbox() async { diff --git a/designer_v2/integration_test/robots/studies_robot.dart b/designer_v2/integration_test/robots/studies_robot.dart index e21634498..56e32f8a2 100644 --- a/designer_v2/integration_test/robots/studies_robot.dart +++ b/designer_v2/integration_test/robots/studies_robot.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:patrol_finders/patrol_finders.dart'; import 'package:studyu_designer_v2/localization/app_translation.dart'; @@ -32,6 +33,6 @@ class StudiesRobot { } Future tapSignOutButton() async { - await $(tr.navlink_logout).tap(); + await $(Icons.logout_rounded).tap(); } } diff --git a/designer_v2/integration_test/robots/study_info_robot.dart b/designer_v2/integration_test/robots/study_info_robot.dart index 15eb8f9a4..619faa960 100644 --- a/designer_v2/integration_test/robots/study_info_robot.dart +++ b/designer_v2/integration_test/robots/study_info_robot.dart @@ -14,90 +14,109 @@ class StudyInfoRobot { Future enterStudyName(String studyName) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_field_study_title) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == tr.form_field_study_title, + ) .scrollTo() .enterText(studyName); } Future enterStudyDescription(String studyDescription) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_field_study_description_hint) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == + tr.form_field_study_description_hint, + ) .scrollTo() .enterText(studyDescription); } Future enterResponsibleOrg(String orgName) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_field_organization) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == tr.form_field_organization, + ) .scrollTo() .enterText(orgName); } Future enterInstitutionalReviewBoard(String irbName) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_field_review_board) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == tr.form_field_review_board, + ) .scrollTo() .enterText(irbName); } Future enterIRBProtocolNumber(String irbNumber) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_field_review_board_number) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == tr.form_field_review_board_number, + ) .scrollTo() .enterText(irbNumber); } Future enterResponsiblePerson(String personName) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_field_researchers) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == tr.form_field_researchers, + ) .scrollTo() .enterText(personName); } Future enterWebsite(String website) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_field_website) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == tr.form_field_website, + ) .scrollTo() .enterText(website); } Future enterContactEmail(String emailAddress) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_field_contact_email) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == tr.form_field_contact_email, + ) .scrollTo() .enterText(emailAddress); } Future enterContactPhone(String phoneNumber) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_field_contact_phone) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == tr.form_field_contact_phone, + ) .scrollTo() .enterText(phoneNumber); } diff --git a/designer_v2/integration_test/robots/study_interventions_robot.dart b/designer_v2/integration_test/robots/study_interventions_robot.dart index ccc3bbc0a..efbea9078 100644 --- a/designer_v2/integration_test/robots/study_interventions_robot.dart +++ b/designer_v2/integration_test/robots/study_interventions_robot.dart @@ -26,40 +26,53 @@ class StudyInterventionsRobot { Future enterInterventionName(String interventionName) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_field_intervention_title) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == tr.form_field_intervention_title, + ) .scrollTo() .enterText(interventionName); } - Future enterInterventionDesciption(String interventionDescription) async { + Future enterInterventionDesciption( + String interventionDescription, + ) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_field_intervention_description_hint) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == + tr.form_field_intervention_description_hint, + ) .scrollTo() .enterText(interventionDescription); } Future enterInterventionTaskName(String taskName) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_field_intervention_task_title) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == + tr.form_field_intervention_task_title, + ) .scrollTo() .enterText(taskName); } Future enterInterventionTaskDescription(String taskDescription) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_field_intervention_task_description_hint) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == + tr.form_field_intervention_task_description_hint, + ) .scrollTo() .enterText(taskDescription); } diff --git a/designer_v2/integration_test/robots/study_measurements_robot.dart b/designer_v2/integration_test/robots/study_measurements_robot.dart index 929c76add..263a41123 100644 --- a/designer_v2/integration_test/robots/study_measurements_robot.dart +++ b/designer_v2/integration_test/robots/study_measurements_robot.dart @@ -26,50 +26,64 @@ class StudyMeasurementsRobot { Future enterSurveyName(String surveyName) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_field_measurement_survey_title) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == + tr.form_field_measurement_survey_title, + ) .scrollTo() .enterText(surveyName); } Future enterSurveyIntroText(String introText) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_field_measurement_survey_intro_text_hint) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == + tr.form_field_measurement_survey_intro_text_hint, + ) .scrollTo() .enterText(introText); } Future enterSurveyOutroText(String outroText) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_field_measurement_survey_outro_text_hint) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == + tr.form_field_measurement_survey_outro_text_hint, + ) .scrollTo() .enterText(outroText); } Future enterSurveyQuestionText(String questionText) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_field_question) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == tr.form_field_question, + ) .scrollTo() .enterText(questionText); } Future enterSurveyQuestionOption1(String optionText) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_array_response_options_choice_hint) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == + tr.form_array_response_options_choice_hint, + ) .at(0) .scrollTo() .enterText(optionText); @@ -77,10 +91,13 @@ class StudyMeasurementsRobot { Future enterSurveyQuestionOption2(String optionText) async { await $(TextField) - .which((widget) => - widget.decoration is InputDecoration && - widget.decoration!.hintText != null && - widget.decoration!.hintText! == tr.form_array_response_options_choice_hint) + .which( + (widget) => + widget.decoration is InputDecoration && + widget.decoration!.hintText != null && + widget.decoration!.hintText! == + tr.form_array_response_options_choice_hint, + ) .at(1) .scrollTo() .enterText(optionText); diff --git a/designer_v2/lib/common_views/action_inline_menu.dart b/designer_v2/lib/common_views/action_inline_menu.dart index 33a5869df..009f00fc5 100644 --- a/designer_v2/lib/common_views/action_inline_menu.dart +++ b/designer_v2/lib/common_views/action_inline_menu.dart @@ -3,18 +3,19 @@ import 'package:studyu_designer_v2/common_views/mouse_events.dart'; import 'package:studyu_designer_v2/utils/model_action.dart'; class ActionMenuInline extends StatelessWidget { - const ActionMenuInline( - {required this.actions, - this.splashRadius = 18.0, - this.iconSize, - this.iconColor, - this.visible = true, - this.paddingHorizontal = 2.0, - this.paddingVertical = 0.0, - super.key}); + const ActionMenuInline({ + required this.actions, + this.splashRadius = 18.0, + this.iconSize, + this.iconColor, + this.visible = true, + this.paddingHorizontal = 2.0, + this.paddingVertical = 0.0, + super.key, + }); final List actions; - final MaterialStateProperty? iconColor; + final WidgetStateProperty? iconColor; final double? iconSize; final bool visible; final double? splashRadius; @@ -30,31 +31,42 @@ class ActionMenuInline extends StatelessWidget { final theme = Theme.of(context); - defaultIconColor(Set states) { - if (states.contains(MaterialState.hovered)) { + Color defaultIconColor(Set states) { + if (states.contains(WidgetState.hovered)) { return theme.colorScheme.secondary.withOpacity(0.8); } return theme.colorScheme.secondary.withOpacity(0.4); } - final actionButtons = actions.map((action) { + final actionButtons = actions.map((ModelAction action) { return Tooltip( - message: action.label, - child: MouseEventsRegion(builder: (context, state) { + message: action.label, + child: MouseEventsRegion( + builder: (context, state) { return IconButton( - padding: EdgeInsets.zero, - splashRadius: splashRadius, - onPressed: () => action.onExecute(), - iconSize: iconSize ?? theme.iconTheme.size ?? 16.0, - icon: Icon(action.icon, - color: iconColor?.resolve(state) ?? (action.isDestructive ? Colors.red : defaultIconColor(state)))); - })); + padding: EdgeInsets.zero, + splashRadius: splashRadius, + onPressed: () => action.onExecute(), + iconSize: iconSize ?? theme.iconTheme.size ?? 16.0, + icon: Icon( + action.icon, + color: iconColor?.resolve(state) ?? + (action.isDestructive + ? Colors.red + : defaultIconColor(state)), + ), + ); + }, + ), + ); }).toList(); return Padding( - padding: EdgeInsets.symmetric(horizontal: paddingHorizontal ?? 0, vertical: paddingVertical ?? 0), + padding: EdgeInsets.symmetric( + horizontal: paddingHorizontal ?? 0, + vertical: paddingVertical ?? 0, + ), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, children: actionButtons, ), ); diff --git a/designer_v2/lib/common_views/action_popup_menu.dart b/designer_v2/lib/common_views/action_popup_menu.dart index e07d61bef..ccdaf7aa7 100644 --- a/designer_v2/lib/common_views/action_popup_menu.dart +++ b/designer_v2/lib/common_views/action_popup_menu.dart @@ -7,19 +7,20 @@ typedef ActionsProviderFor = List Function(T from); typedef ActionsProviderAt = List Function(T from, int idx); class ActionPopUpMenuButton extends StatelessWidget { - const ActionPopUpMenuButton( - {required this.actions, - this.orientation = Axis.horizontal, - this.elevation = 5, - this.splashRadius = 24.0, - this.triggerIconSize = 18.0, - this.position = PopupMenuPosition.under, - this.triggerIconColor, - this.triggerIconColorHover, - this.disableSplashEffect = false, - this.hideOnEmpty = true, - this.enabled = true, - super.key}); + const ActionPopUpMenuButton({ + required this.actions, + this.orientation = Axis.horizontal, + this.elevation = 5, + this.splashRadius = 24.0, + this.triggerIconSize = 18.0, + this.position = PopupMenuPosition.under, + this.triggerIconColor, + this.triggerIconColorHover, + this.disableSplashEffect = false, + this.hideOnEmpty = true, + this.enabled = true, + super.key, + }); final List actions; final Color? triggerIconColor; @@ -39,57 +40,75 @@ class ActionPopUpMenuButton extends StatelessWidget { return const SizedBox.shrink(); } - return MouseEventsRegion(builder: (context, state) { - Widget widget = _buildPopupMenu(context, state); + return MouseEventsRegion( + builder: (context, state) { + Widget widget = _buildPopupMenu(context, state); - if (disableSplashEffect) { - final popupMenu = widget; - widget = Theme( + if (disableSplashEffect) { + final popupMenu = widget; + widget = Theme( data: Theme.of(context).copyWith( splashColor: Colors.transparent, highlightColor: Colors.transparent, hoverColor: Colors.transparent, ), - child: popupMenu); - } + child: popupMenu, + ); + } - return widget; - }); + return widget; + }, + ); } - Widget _buildPopupMenu(BuildContext context, Set state) { + Widget _buildPopupMenu(BuildContext context, Set state) { final theme = Theme.of(context); - final isHovered = state.contains(MaterialState.hovered); - final iconColorDefault = triggerIconColor ?? theme.iconTheme.color!.withOpacity(0.7); - final iconColorHover = triggerIconColorHover ?? theme.iconTheme.color!.withOpacity(0.7); - final triggerIcon = (orientation == Axis.vertical) ? Icons.more_vert_rounded : Icons.more_horiz_rounded; + final isHovered = state.contains(WidgetState.hovered); + final iconColorDefault = + triggerIconColor ?? theme.iconTheme.color!.withOpacity(0.7); + final iconColorHover = + triggerIconColorHover ?? theme.iconTheme.color!.withOpacity(0.7); + final triggerIcon = (orientation == Axis.vertical) + ? Icons.more_vert_rounded + : Icons.more_horiz_rounded; return PopupMenuButton( - icon: Icon(triggerIcon, size: triggerIconSize, color: (isHovered) ? iconColorHover : iconColorDefault), - enabled: enabled, - elevation: elevation, - splashRadius: splashRadius, - position: position, - onSelected: (ModelAction action) => action.onExecute(), - itemBuilder: (BuildContext context) { - final textTheme = theme.textTheme.labelMedium!; - return actions.map((action) { - return PopupMenuItem( - value: action, - child: ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 0), - horizontalTitleGap: 4.0, - leading: (action.icon == null) - ? const SizedBox.shrink() - : Icon(action.icon, - size: theme.iconTheme.size ?? 14.0, - color: action.isDestructive ? Colors.red : iconColorDefault), - title: action.isDestructive - ? Text(action.label, style: textTheme.copyWith(color: Colors.red)) - : Text(action.label, style: textTheme), - ), - ); - }).toList(); - }); + icon: Icon( + triggerIcon, + size: triggerIconSize, + color: isHovered ? iconColorHover : iconColorDefault, + ), + enabled: enabled, + elevation: elevation, + splashRadius: splashRadius, + position: position, + onSelected: (ModelAction action) => action.onExecute(), + itemBuilder: (BuildContext context) { + final textTheme = theme.textTheme.labelMedium!; + return actions.map((action) { + return PopupMenuItem( + value: action, + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), + horizontalTitleGap: 4.0, + leading: (action.icon == null) + ? const SizedBox.shrink() + : Icon( + action.icon, + size: theme.iconTheme.size ?? 14.0, + color: + action.isDestructive ? Colors.red : iconColorDefault, + ), + title: action.isDestructive + ? Text( + action.label, + style: textTheme.copyWith(color: Colors.red), + ) + : Text(action.label, style: textTheme), + ), + ); + }).toList(); + }, + ); } } diff --git a/designer_v2/lib/common_views/async_value_widget.dart b/designer_v2/lib/common_views/async_value_widget.dart index 63f732bfe..680a46dff 100644 --- a/designer_v2/lib/common_views/async_value_widget.dart +++ b/designer_v2/lib/common_views/async_value_widget.dart @@ -4,7 +4,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; /// Simple wrapper around [AsyncValue] to render standardized /// widgets for different states (loading, error, empty) class AsyncValueWidget extends StatelessWidget { - const AsyncValueWidget({super.key, required this.value, required this.data, this.error, this.loading, this.empty}); + const AsyncValueWidget({ + super.key, + required this.value, + required this.data, + this.error, + this.loading, + this.empty, + }); final AsyncValue value; final Widget Function(T) data; @@ -28,7 +35,8 @@ class AsyncValueWidget extends StatelessWidget { // Always render data widget if no empty state specified return data(unwrappedData); } - if (unwrappedData == null || (unwrappedData is List && unwrappedData.isEmpty)) { + if (unwrappedData == null || + (unwrappedData is List && unwrappedData.isEmpty)) { return empty!(); } return data(unwrappedData); diff --git a/designer_v2/lib/common_views/badge.dart b/designer_v2/lib/common_views/badge.dart index 547e2720a..ac7f021f2 100644 --- a/designer_v2/lib/common_views/badge.dart +++ b/designer_v2/lib/common_views/badge.dart @@ -32,50 +32,58 @@ class Badge extends StatelessWidget { final theme = Theme.of(context); return IntrinsicWidth( - child: Container( - decoration: BoxDecoration( - color: Colors.white, // solid background to paint over - borderRadius: BorderRadius.all(Radius.circular(borderRadius)), - ), child: Container( decoration: BoxDecoration( - color: _getBackgroundColor(theme), + color: Colors.white, // solid background to paint over borderRadius: BorderRadius.all(Radius.circular(borderRadius)), - border: Border.all(color: _getBorderColor(theme)), ), - child: Padding( - padding: padding, - child: Row( - mainAxisAlignment: center ? MainAxisAlignment.center : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - (icon != null) - ? Icon( - icon, - size: iconSize ?? ((theme.iconTheme.size ?? 14.0) * 0.8), - color: _getLabelColor(theme)?.faded(0.65), - ) - : const SizedBox.shrink(), - (icon != null) ? const SizedBox(width: 8.0) : const SizedBox.shrink(), - Expanded( + child: Container( + decoration: BoxDecoration( + color: _getBackgroundColor(theme), + borderRadius: BorderRadius.all(Radius.circular(borderRadius)), + border: Border.all(color: _getBorderColor(theme)), + ), + child: Padding( + padding: padding, + child: Row( + mainAxisAlignment: + center ? MainAxisAlignment.center : MainAxisAlignment.start, + children: [ + if (icon != null) + Icon( + icon, + size: iconSize ?? ((theme.iconTheme.size ?? 14.0) * 0.8), + color: _getLabelColor(theme)?.faded(0.65), + ) + else + const SizedBox.shrink(), + if (icon != null) + const SizedBox(width: 8.0) + else + const SizedBox.shrink(), + Expanded( child: Text( - label, - softWrap: false, - maxLines: 1, - textAlign: TextAlign.center, - style: theme.textTheme.bodySmall - ?.copyWith( - fontSize: (theme.textTheme.bodySmall?.fontSize ?? 14.0) * 0.95, - color: _getLabelColor(theme), - fontWeight: FontWeight.bold, - ) - .merge(labelStyle), - )), - ], + label, + softWrap: false, + maxLines: 1, + textAlign: TextAlign.center, + style: theme.textTheme.bodySmall + ?.copyWith( + fontSize: + (theme.textTheme.bodySmall?.fontSize ?? 14.0) * + 0.95, + color: _getLabelColor(theme), + fontWeight: FontWeight.bold, + ) + .merge(labelStyle), + ), + ), + ], + ), ), ), ), - )); + ); } Color? _getBackgroundColor(ThemeData theme) { diff --git a/designer_v2/lib/common_views/banner.dart b/designer_v2/lib/common_views/banner.dart index 4b072f78b..5a98ad01f 100644 --- a/designer_v2/lib/common_views/banner.dart +++ b/designer_v2/lib/common_views/banner.dart @@ -10,17 +10,18 @@ abstract class IWithBanner { enum BannerStyle { warning, info, error } class BannerBox extends StatefulWidget { - const BannerBox( - {required this.body, - required this.style, - this.padding = const EdgeInsets.symmetric(vertical: 18.0, horizontal: 48.0), - this.prefixIcon, - this.noPrefix = false, - this.isDismissed, - this.dismissable = true, - this.onDismissed, - this.dismissIconSize = 24.0, - super.key}); + const BannerBox({ + required this.body, + required this.style, + this.padding = const EdgeInsets.symmetric(vertical: 18.0, horizontal: 48.0), + this.prefixIcon, + this.noPrefix = false, + this.isDismissed, + this.dismissable = true, + this.onDismissed, + this.dismissIconSize = 24.0, + super.key, + }); final Widget? prefixIcon; final Widget body; @@ -73,26 +74,31 @@ class _BannerBoxState extends State { child: Row( children: [ Expanded( - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - (widget.noPrefix) ? const SizedBox.shrink() : icon, - (widget.noPrefix) ? const SizedBox.shrink() : const SizedBox(width: 24.0), - Opacity( - opacity: 0.85, - child: widget.body, - ), - ], - )), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + if (widget.noPrefix) const SizedBox.shrink() else icon, + if (widget.noPrefix) + const SizedBox.shrink() + else + const SizedBox(width: 24.0), + Opacity( + opacity: 0.85, + child: widget.body, + ), + ], + ), + ), SizedBox( height: double.infinity, child: Opacity( opacity: 0.5, child: IconButton( - icon: Icon(Icons.close_rounded, size: widget.dismissIconSize), + icon: + Icon(Icons.close_rounded, size: widget.dismissIconSize), splashRadius: widget.dismissIconSize, onPressed: () => setState(() { - if (widget.onDismissed != null) widget.onDismissed!(); + widget.onDismissed?.call(); isDismissed = true; }), ), diff --git a/designer_v2/lib/common_views/collapse.dart b/designer_v2/lib/common_views/collapse.dart index 7b4ad7402..8b38f9ce8 100644 --- a/designer_v2/lib/common_views/collapse.dart +++ b/designer_v2/lib/common_views/collapse.dart @@ -3,9 +3,12 @@ import 'package:studyu_designer_v2/common_views/form_table_layout.dart'; import 'package:studyu_designer_v2/common_views/mouse_events.dart'; import 'package:studyu_designer_v2/common_views/utils.dart'; -import '../theme.dart'; +import 'package:studyu_designer_v2/theme.dart'; -typedef CollapsibleSectionBuilder = Widget Function(BuildContext context, bool isCollapsed); +typedef CollapsibleSectionBuilder = Widget Function( + BuildContext context, + bool isCollapsed, +); /// Simple non-animated & more customizable alternative to [ExpansionPanel] /// and [ExpansionTile] @@ -21,8 +24,11 @@ class Collapsible extends StatefulWidget { this.isCollapsed = true, this.maintainState = true, super.key, - }) : assert((headerBuilder != null && title == null) || (headerBuilder == null && title != null), - "Must provide either headerBuilder or title"); + }) : assert( + (headerBuilder != null && title == null) || + (headerBuilder == null && title != null), + "Must provide either headerBuilder or title", + ); final CollapsibleSectionBuilder contentBuilder; final CollapsibleSectionBuilder? headerBuilder; @@ -45,7 +51,7 @@ class _CollapsibleState extends State { final headerWidget = widget.headerBuilder?.call(context, isCollapsed) ?? MouseEventsRegion( builder: (context, states) { - final isHovered = states.contains(MaterialState.hovered); + final isHovered = states.contains(WidgetState.hovered); // Use [TabBarThemeData] colors for default header styling /*Color? actualColor = isHovered @@ -57,13 +63,16 @@ class _CollapsibleState extends State { child: Row( children: [ FormLabel( - labelText: widget.title!, + labelText: widget.title, //labelTextStyle: TextStyle(color: actualColor), ), const SizedBox(width: 4.0), Icon( - isCollapsed ? Icons.keyboard_arrow_right_rounded : Icons.keyboard_arrow_down_rounded, - color: theme.tabBarTheme.labelColor?.faded(ThemeConfig.kMuteFadeFactor), + isCollapsed + ? Icons.keyboard_arrow_right_rounded + : Icons.keyboard_arrow_down_rounded, + color: theme.tabBarTheme.labelColor + ?.faded(ThemeConfig.kMuteFadeFactor), ), ], ), @@ -80,7 +89,7 @@ class _CollapsibleState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ headerWidget, - !isCollapsed ? contentWidget : const SizedBox.shrink(), + if (!isCollapsed) contentWidget else const SizedBox.shrink(), ], ); } diff --git a/designer_v2/lib/common_views/constrained_flexible.dart b/designer_v2/lib/common_views/constrained_flexible.dart index fcec8cdac..01343db43 100644 --- a/designer_v2/lib/common_views/constrained_flexible.dart +++ b/designer_v2/lib/common_views/constrained_flexible.dart @@ -3,14 +3,15 @@ import 'package:flutter/widgets.dart'; /// Taken from /// https://stackoverflow.com/questions/56417186/specific-min-and-max-size-for-expanded-widgets-in-column class ConstrainedWidthFlexible extends StatelessWidget { - const ConstrainedWidthFlexible( - {required this.minWidth, - required this.maxWidth, - required this.flex, - required this.flexSum, - required this.outerConstraints, - required this.child, - super.key}); + const ConstrainedWidthFlexible({ + required this.minWidth, + required this.maxWidth, + required this.flex, + required this.flexSum, + required this.outerConstraints, + required this.child, + super.key, + }); final double minWidth; final double maxWidth; diff --git a/designer_v2/lib/common_views/dialog.dart b/designer_v2/lib/common_views/dialog.dart index 431fa4500..54109da1a 100644 --- a/designer_v2/lib/common_views/dialog.dart +++ b/designer_v2/lib/common_views/dialog.dart @@ -63,10 +63,9 @@ class StandardDialog extends StatelessWidget { boxShadow: [ BoxShadow( color: theme.shadowColor, - spreadRadius: 0, blurRadius: 3, offset: const Offset(1, 1), - ) + ), ], ), child: Container( @@ -94,8 +93,14 @@ class StandardDialog extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - (titleWidget != null) ? titleWidget : const SizedBox.shrink(), - (titleWidget != null) ? SizedBox(height: padding.top * 2 / 3) : const SizedBox.shrink(), + if (titleWidget != null) + titleWidget + else + const SizedBox.shrink(), + if (titleWidget != null) + SizedBox(height: padding.top * 2 / 3) + else + const SizedBox.shrink(), Expanded( child: SingleChildScrollView(child: body), ), diff --git a/designer_v2/lib/common_views/empty_body.dart b/designer_v2/lib/common_views/empty_body.dart index bff2fd096..75fb3a296 100644 --- a/designer_v2/lib/common_views/empty_body.dart +++ b/designer_v2/lib/common_views/empty_body.dart @@ -26,40 +26,50 @@ class EmptyBody extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - (leading != null) ? leading! : const SizedBox.shrink(), - (leading != null) ? SizedBox(height: leadingSpacing!) : const SizedBox.shrink(), - (icon != null) - ? Padding( - padding: const EdgeInsets.only(bottom: 0.0), - child: Icon( - icon, - size: 96.0, - color: theme.colorScheme.secondary, - )) - : const SizedBox.shrink(), - (title != null) - ? Padding( - padding: const EdgeInsets.symmetric(vertical: 6.0), - child: SelectableText( - title!, - textAlign: TextAlign.center, - style: theme.textTheme.headlineMedium, - ), - ) - : const SizedBox.shrink(), - (description != null) - ? SelectableText(description!, - textAlign: TextAlign.center, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodyMedium?.color?.faded(0.9), - )) - : const SizedBox.shrink(), - (button != null) - ? Padding( - padding: const EdgeInsets.fromLTRB(0, 20.0, 0, 16.0), - child: button, - ) - : const SizedBox.shrink(), + if (leading != null) leading! else const SizedBox.shrink(), + if (leading != null) + SizedBox(height: leadingSpacing) + else + const SizedBox.shrink(), + if (icon != null) + Padding( + padding: EdgeInsets.zero, + child: Icon( + icon, + size: 96.0, + color: theme.colorScheme.secondary, + ), + ) + else + const SizedBox.shrink(), + if (title != null) + Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: SelectableText( + title!, + textAlign: TextAlign.center, + style: theme.textTheme.headlineMedium, + ), + ) + else + const SizedBox.shrink(), + if (description != null) + SelectableText( + description!, + textAlign: TextAlign.center, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.textTheme.bodyMedium?.color?.faded(0.9), + ), + ) + else + const SizedBox.shrink(), + if (button != null) + Padding( + padding: const EdgeInsets.fromLTRB(0, 20.0, 0, 16.0), + child: button, + ) + else + const SizedBox.shrink(), ], ), ); diff --git a/designer_v2/lib/common_views/form_buttons.dart b/designer_v2/lib/common_views/form_buttons.dart index d859da87b..3ed9b3d22 100644 --- a/designer_v2/lib/common_views/form_buttons.dart +++ b/designer_v2/lib/common_views/form_buttons.dart @@ -28,58 +28,76 @@ class DismissButton extends StatelessWidget { Widget build(BuildContext context) { assert(debugCheckHasMaterialLocalizations(context)); return KeyboardListener( - focusNode: FocusNode(), - autofocus: true, - onKeyEvent: (key) { - if (key.logicalKey.keyLabel == "Escape") { + focusNode: FocusNode(), + autofocus: true, + onKeyEvent: (key) { + if (key.logicalKey.keyLabel == "Escape") { + Navigator.maybePop(context); + } + }, + child: SecondaryButton( + text: text ?? tr.dialog_cancel, + icon: null, + //tooltip: MaterialLocalizations.of(context).closeButtonTooltip, + onPressed: () { + if (onPressed != null) { + onPressed!(); + } else { Navigator.maybePop(context); } }, - child: SecondaryButton( - text: text ?? tr.dialog_cancel, - icon: null, - //tooltip: MaterialLocalizations.of(context).closeButtonTooltip, - onPressed: () { - if (onPressed != null) { - onPressed!(); - } else { - Navigator.maybePop(context); - } - }, - )); + ), + ); } } List buildFormButtons(FormViewModel formViewModel, FormMode formMode) { final modifyActionButtons = [ - ReactiveFormConsumer(// enable re-rendering based on form validation status - builder: (context, form, child) { - return retainSizeInAppBar(DismissButton( - onPressed: () => formViewModel.cancel().then((_) => Navigator.maybePop(context)), - )); - }), - ReactiveFormConsumer(// enable re-rendering based on form validation status - builder: (context, form, child) { - return retainSizeInAppBar(PrimaryButton( - text: tr.dialog_save, - tooltipDisabled: "${tr.form_invalid_prompt}\n\n${formViewModel.form.validationErrorSummary}", - icon: null, - enabled: formViewModel.isValid, - onPressedFuture: (formViewModel.isValid) ? () => formViewModel.save().then( - // Close the form (side sheet or scaffold route) if future - // completed successfully - (value) => Navigator.maybePop(context)) : null, - )); - }), + ReactiveFormConsumer( + // enable re-rendering based on form validation status + builder: (context, form, child) { + return retainSizeInAppBar( + DismissButton( + onPressed: () => + formViewModel.cancel().then((_) => Navigator.maybePop(context)), + ), + ); + }, + ), + ReactiveFormConsumer( + // enable re-rendering based on form validation status + builder: (context, form, child) { + return retainSizeInAppBar( + PrimaryButton( + text: tr.dialog_save, + tooltipDisabled: + "${tr.form_invalid_prompt}\n\n${formViewModel.form.validationErrorSummary}", + icon: null, + enabled: formViewModel.isValid, + onPressedFuture: (formViewModel.isValid) + ? () => formViewModel.save().then( + // Close the form (side sheet or scaffold route) if future + // completed successfully + (value) => Navigator.maybePop(context), + ) + : null, + ), + ); + }, + ), ]; final readonlyActionButtons = [ - ReactiveFormConsumer(// enable re-rendering based on form validation status - builder: (context, form, child) { - return retainSizeInAppBar(DismissButton( - text: tr.dialog_close, - onPressed: () => Navigator.maybePop(context), - )); - }), + ReactiveFormConsumer( + // enable re-rendering based on form validation status + builder: (context, form, child) { + return retainSizeInAppBar( + DismissButton( + text: tr.dialog_close, + onPressed: () => Navigator.maybePop(context), + ), + ); + }, + ), ]; final defaultActionButtons = { diff --git a/designer_v2/lib/common_views/form_consumer_widget.dart b/designer_v2/lib/common_views/form_consumer_widget.dart index 02e5dfa7a..3e27f4660 100644 --- a/designer_v2/lib/common_views/form_consumer_widget.dart +++ b/designer_v2/lib/common_views/form_consumer_widget.dart @@ -33,9 +33,11 @@ abstract class FormConsumerWidget extends StatefulWidget { class _FormConsumerWidgetState extends State { @override Widget build(BuildContext context) { - return ReactiveFormConsumer(builder: (context, form, _) { - return widget.build(context, form); - }); + return ReactiveFormConsumer( + builder: (context, form, _) { + return widget.build(context, form); + }, + ); } } @@ -47,14 +49,17 @@ abstract class FormConsumerRefWidget extends ConsumerStatefulWidget { Widget build(BuildContext context, FormGroup form, WidgetRef ref); @override - ConsumerState createState() => _FormConsumerRefWidgetState(); + ConsumerState createState() => + _FormConsumerRefWidgetState(); } class _FormConsumerRefWidgetState extends ConsumerState { @override Widget build(BuildContext context) { - return ReactiveFormConsumer(builder: (context, form, _) { - return widget.build(context, form, ref); - }); + return ReactiveFormConsumer( + builder: (context, form, _) { + return widget.build(context, form, ref); + }, + ); } } diff --git a/designer_v2/lib/common_views/form_control_label.dart b/designer_v2/lib/common_views/form_control_label.dart index f8aa37554..38d9d2f4b 100644 --- a/designer_v2/lib/common_views/form_control_label.dart +++ b/designer_v2/lib/common_views/form_control_label.dart @@ -2,16 +2,19 @@ import 'package:flutter/material.dart'; import 'package:reactive_forms/reactive_forms.dart'; import 'package:studyu_designer_v2/common_views/mouse_events.dart'; -typedef FormControlVoidCallback = void Function(AbstractControl formControl); +typedef FormControlVoidCallback = void Function( + AbstractControl formControl, +); class FormControlLabel extends StatelessWidget { - const FormControlLabel( - {required this.formControl, - required this.text, - this.textStyle, - this.isClickable = true, - this.onClick, - super.key}); + const FormControlLabel({ + required this.formControl, + required this.text, + this.textStyle, + this.isClickable = true, + this.onClick, + super.key, + }); final AbstractControl formControl; final String text; @@ -22,13 +25,16 @@ class FormControlLabel extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final stateColorStyle = (formControl.disabled) ? TextStyle(color: theme.disabledColor) : null; + final stateColorStyle = + (formControl.disabled) ? TextStyle(color: theme.disabledColor) : null; return MouseEventsRegion( builder: (context, states) { return Text( text, - style: theme.textTheme.bodySmall?.merge(textStyle).merge(stateColorStyle), + style: theme.textTheme.bodySmall + ?.merge(textStyle) + .merge(stateColorStyle), overflow: TextOverflow.clip, ); }, @@ -40,7 +46,8 @@ class FormControlLabel extends StatelessWidget { } else { if (formControl is AbstractControl) { // Auto-toggle boolean controls - formControl.value = (formControl.value != null) ? !(formControl.value!) : true; + formControl.value = + formControl.value == null || !(formControl.value as bool); formControl.markAsDirty(); } else { // Otherwise just focus the control diff --git a/designer_v2/lib/common_views/form_scaffold.dart b/designer_v2/lib/common_views/form_scaffold.dart index 5b8577374..d5b0e3cf2 100644 --- a/designer_v2/lib/common_views/form_scaffold.dart +++ b/designer_v2/lib/common_views/form_scaffold.dart @@ -10,21 +10,26 @@ import 'package:studyu_designer_v2/theme.dart'; /// Signature for a builder that renders the widget corresponding to the /// [FormViewModel] of type [T] -typedef FormViewBuilder = Widget Function(T formViewModel); +typedef FormViewBuilder = Widget Function( + T formViewModel, +); /// Signature for a builder that resolves the [FormViewModel] of type [T] /// via a Riverpod [WidgetRef] -typedef FormViewModelBuilder = T Function(WidgetRef ref); +typedef FormViewModelBuilder = T Function( + WidgetRef ref, +); class FormScaffold extends ConsumerStatefulWidget { - const FormScaffold( - {required this.formViewModel, - required this.body, - this.actions, - this.drawer, - this.actionsSpacing = 8.0, - this.actionsPadding = 24.0, - super.key}); + const FormScaffold({ + required this.formViewModel, + required this.body, + this.actions, + this.drawer, + this.actionsSpacing = 8.0, + this.actionsPadding = 24.0, + super.key, + }); final T formViewModel; final List? actions; @@ -37,7 +42,8 @@ class FormScaffold extends ConsumerStatefulWidget { ConsumerState> createState() => _FormScaffoldState(); } -class _FormScaffoldState extends ConsumerState> implements PopEntry { +class _FormScaffoldState + extends ConsumerState> implements PopEntry { T get formViewModel => widget.formViewModel; ModalRoute? _route; @@ -65,7 +71,9 @@ class _FormScaffoldState extends ConsumerState extends ConsumerState? iconPack}) { + static IconOption? resolveIconByName( + String? name, { + List? iconPack, + }) { iconPack ??= IconPack.defaultPack; if (name == null || name.isEmpty) { return null; @@ -48,14 +51,15 @@ class IconOption extends Equatable { List get props => [name]; String toJson() => name; - static IconOption fromJson(String json) => IconOption(json); + IconOption fromJson(String json) => IconOption(json); } -class ReactiveIconPicker extends ReactiveFocusableFormField { +class ReactiveIconPicker + extends ReactiveFocusableFormField { ReactiveIconPicker({ - required iconOptions, - selectedIconSize = 20.0, - galleryIconSize = 28.0, + required List iconOptions, + double? selectedIconSize = 20.0, + double? galleryIconSize = 28.0, bool readOnly = false, ReactiveFormFieldCallback? onSelect, super.formControl, @@ -64,11 +68,12 @@ class ReactiveIconPicker extends ReactiveFocusableFormField field) { - // Unsupported: showErrors, validationMessages - final isDisabled = readOnly || field.control.disabled; + }) : super( + builder: (ReactiveFormFieldState field) { + // Unsupported: showErrors, validationMessages + final isDisabled = readOnly || field.control.disabled; - return IconPicker( + return IconPicker( iconOptions: iconOptions, isDisabled: isDisabled, focusNode: focusNode, @@ -79,8 +84,10 @@ class ReactiveIconPicker extends ReactiveFocusableFormField iconOptions; @@ -144,25 +152,36 @@ class IconPickerField extends StatelessWidget { @override Widget build(BuildContext context) { - final actualGalleryIconSize = galleryIconSize ?? Theme.of(context).iconTheme.size ?? 24.0; - final actualSelectedIconSize = selectedIconSize ?? Theme.of(context).iconTheme.size ?? 16.0; - - openIconPicker() => showIconPickerDialog(context, - iconOptions: iconOptions, galleryIconSize: actualGalleryIconSize, onSelect: onSelect); + final actualGalleryIconSize = + galleryIconSize ?? Theme.of(context).iconTheme.size ?? 24.0; + final actualSelectedIconSize = + selectedIconSize ?? Theme.of(context).iconTheme.size ?? 16.0; + + Future openIconPicker() => showIconPickerDialog( + context, + iconOptions: iconOptions, + galleryIconSize: actualGalleryIconSize, + onSelect: onSelect, + ); if (selectedOption != null && !selectedOption!.isEmpty) { - final selectedIcon = - selectedOption?.icon ?? IconPack.resolveIconByName(selectedOption!.name, iconPack: iconOptions)!.icon; + final selectedIcon = selectedOption?.icon ?? + IconPack.resolveIconByName( + selectedOption!.name, + iconPack: iconOptions, + )! + .icon; return IconButton( - tooltip: tr.iconpicker_nonempty_prompt, - splashRadius: actualSelectedIconSize, - onPressed: (isDisabled) ? null : openIconPicker, - focusNode: focusNode, - icon: Icon(selectedIcon, size: actualSelectedIconSize)); + tooltip: tr.iconpicker_nonempty_prompt, + splashRadius: actualSelectedIconSize, + onPressed: isDisabled ? null : openIconPicker, + focusNode: focusNode, + icon: Icon(selectedIcon, size: actualSelectedIconSize), + ); } return TextButton( - onPressed: (isDisabled) ? null : openIconPicker, + onPressed: isDisabled ? null : openIconPicker, focusNode: focusNode, child: Text(tr.iconpicker_empty_prompt), ); @@ -170,7 +189,12 @@ class IconPickerField extends StatelessWidget { } class IconPickerGallery extends StatelessWidget { - const IconPickerGallery({required this.iconOptions, required this.iconSize, this.onSelect, super.key}); + const IconPickerGallery({ + required this.iconOptions, + required this.iconSize, + this.onSelect, + super.key, + }); final List iconOptions; final VoidCallbackOn? onSelect; @@ -181,24 +205,28 @@ class IconPickerGallery extends StatelessWidget { final List iconWidgets = []; for (final iconOption in iconOptions) { final iconWidget = MouseEventsRegion( - builder: (context, state) { - final isHovered = state.contains(MaterialState.hovered); - return Container( - color: isHovered ? Theme.of(context).colorScheme.primary.withOpacity(0.2) : null, - child: Icon(iconOption.icon!, size: iconSize), - ); - }, - onTap: () => Navigator.pop(context, iconOption)); + builder: (context, state) { + final isHovered = state.contains(WidgetState.hovered); + return Container( + color: isHovered + ? Theme.of(context).colorScheme.primary.withOpacity(0.2) + : null, + child: Icon(iconOption.icon, size: iconSize), + ); + }, + onTap: () => Navigator.pop(context, iconOption), + ); iconWidgets.add(iconWidget); } return GridView.extent( - primary: false, - maxCrossAxisExtent: iconSize * 2, - crossAxisSpacing: 4.0, - mainAxisSpacing: 4.0, - //padding: const EdgeInsets.all(12.0), - children: iconWidgets); + primary: false, + maxCrossAxisExtent: iconSize * 2, + crossAxisSpacing: 4.0, + mainAxisSpacing: 4.0, + //padding: const EdgeInsets.all(12.0), + children: iconWidgets, + ); } } @@ -207,10 +235,10 @@ Future showIconPickerDialog( required List iconOptions, double? galleryIconSize, VoidCallbackOn? onSelect, - minWidth = 300, - minHeight = 300, + double minWidth = 300, + double minHeight = 300, }) async { - IconOption? iconPicked = await showDialog( + final IconOption? iconPicked = await showDialog( context: context, builder: (BuildContext context) { final theme = Theme.of(context); @@ -218,18 +246,22 @@ Future showIconPickerDialog( final dialogHeight = MediaQuery.of(context).size.height * 0.4; return StandardDialog( - body: SizedBox( - width: max(dialogWidth, minWidth), - height: max(dialogHeight, minHeight), - child: IconPickerGallery(iconOptions: iconOptions, iconSize: galleryIconSize ?? 48.0), + body: SizedBox( + width: max(dialogWidth, minWidth), + height: max(dialogHeight, minHeight), + child: IconPickerGallery( + iconOptions: iconOptions, + iconSize: galleryIconSize ?? 48.0, + ), + ), + title: SelectableText( + tr.iconpicker_dialog_title, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.normal, + color: theme.colorScheme.onPrimaryContainer, ), - title: SelectableText( - tr.iconpicker_dialog_title, - style: theme.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.normal, - color: theme.colorScheme.onPrimaryContainer, - ), - )); + ), + ); }, ); diff --git a/designer_v2/lib/common_views/icons.dart b/designer_v2/lib/common_views/icons.dart index 37789377c..9169783df 100644 --- a/designer_v2/lib/common_views/icons.dart +++ b/designer_v2/lib/common_views/icons.dart @@ -14,7 +14,9 @@ class HelpIcon extends StatelessWidget { message: tooltipText, child: MouseEventsRegion( builder: (context, states) { - final iconColor = theme.iconTheme.color?.withOpacity((states.contains(MaterialState.hovered)) ? 0.6 : 0.35) ?? + final iconColor = theme.iconTheme.color?.withOpacity( + (states.contains(WidgetState.hovered)) ? 0.6 : 0.35, + ) ?? theme.colorScheme.onSurface.withOpacity(0.3); return Icon( Icons.help_outline_rounded, diff --git a/designer_v2/lib/common_views/layout_single_column.dart b/designer_v2/lib/common_views/layout_single_column.dart index 62b68d5af..43317d269 100644 --- a/designer_v2/lib/common_views/layout_single_column.dart +++ b/designer_v2/lib/common_views/layout_single_column.dart @@ -1,4 +1,5 @@ import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:studyu_designer_v2/common_views/layout_two_column.dart'; import 'package:studyu_designer_v2/theme.dart'; @@ -38,11 +39,11 @@ class SingleColumnLayout extends StatefulWidget { final bool scroll; final EdgeInsets? padding; - static fromType({ + factory SingleColumnLayout.fromType({ required SingleColumnLayoutType type, required Widget body, required BuildContext context, - stickyHeader = false, + bool stickyHeader = false, Widget? header, }) { switch (type) { @@ -75,7 +76,6 @@ class SingleColumnLayout extends StatefulWidget { body: body, header: header, stickyHeader: stickyHeader, - constraints: defaultConstraints, ); case SingleColumnLayoutType.boundedNarrow: return SingleColumnLayout( @@ -126,7 +126,8 @@ class _SingleColumnLayoutState extends State { return Scrollbar( thumbVisibility: true, controller: _scrollController, - child: SingleChildScrollView(controller: _scrollController, child: child), + child: + SingleChildScrollView(controller: _scrollController, child: child), ); } @@ -136,7 +137,6 @@ class _SingleColumnLayoutState extends State { } if (widget.header != null) { body = Column( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ widget.header!, @@ -150,7 +150,6 @@ class _SingleColumnLayoutState extends State { // non-sticky header if (widget.header != null) { body = Column( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ widget.header!, diff --git a/designer_v2/lib/common_views/layout_two_column.dart b/designer_v2/lib/common_views/layout_two_column.dart index 9d2d854a4..05777f2c7 100644 --- a/designer_v2/lib/common_views/layout_two_column.dart +++ b/designer_v2/lib/common_views/layout_two_column.dart @@ -8,7 +8,8 @@ class TwoColumnLayout extends StatefulWidget { this.headerWidget, this.dividerWidget = defaultDivider, this.flexLeft, - this.flexRight = 1, // expand right column to fill available space by default + this.flexRight = + 1, // expand right column to fill available space by default this.constraintsLeft, this.constraintsRight, this.scrollLeft = true, @@ -69,8 +70,8 @@ class TwoColumnLayout extends StatefulWidget { BoxConstraints? constraintsRight, bool scrollLeft = true, bool scrollRight = true, - final EdgeInsets? paddingLeft = TwoColumnLayout.defaultContentPadding, - final EdgeInsets? paddingRight = TwoColumnLayout.defaultContentPadding, + EdgeInsets? paddingLeft = TwoColumnLayout.defaultContentPadding, + EdgeInsets? paddingRight = TwoColumnLayout.defaultContentPadding, }) { return TwoColumnLayout( leftWidget: leftWidget, @@ -112,101 +113,115 @@ class _TwoColumnLayoutState extends State { } if (widget.backgroundColorLeft != null) { - leftWidget = Material(color: widget.backgroundColorLeft, child: leftWidget); + leftWidget = + Material(color: widget.backgroundColorLeft, child: leftWidget); } if (widget.backgroundColorRight != null) { - rightWidget = Material(color: widget.backgroundColorRight, child: rightWidget); + rightWidget = + Material(color: widget.backgroundColorRight, child: rightWidget); } - return LayoutBuilder(builder: (context, constraints) { - if (widget.stretchHeight) { - leftWidget = SizedBox( - height: constraints.maxHeight, - child: leftWidget, - ); - rightWidget = SizedBox( - height: constraints.maxHeight, - child: rightWidget, - ); - } + return LayoutBuilder( + builder: (context, constraints) { + if (widget.stretchHeight) { + leftWidget = SizedBox( + height: constraints.maxHeight, + child: leftWidget, + ); + rightWidget = SizedBox( + height: constraints.maxHeight, + child: rightWidget, + ); + } - if (widget.scrollLeft) { - leftWidget = Scrollbar( - thumbVisibility: true, - controller: _scrollControllerLeft, - child: SingleChildScrollView(controller: _scrollControllerLeft, child: leftWidget), - ); - } - if (widget.scrollRight) { - rightWidget = Scrollbar( - thumbVisibility: true, - controller: _scrollControllerRight, - child: SingleChildScrollView(controller: _scrollControllerRight, child: rightWidget), - ); - } + if (widget.scrollLeft) { + leftWidget = Scrollbar( + thumbVisibility: true, + controller: _scrollControllerLeft, + child: SingleChildScrollView( + controller: _scrollControllerLeft, + child: leftWidget, + ), + ); + } + if (widget.scrollRight) { + rightWidget = Scrollbar( + thumbVisibility: true, + controller: _scrollControllerRight, + child: SingleChildScrollView( + controller: _scrollControllerRight, + child: rightWidget, + ), + ); + } - if (!(widget.constraintsLeft != null && widget.flexLeft != null)) { - if (widget.constraintsLeft != null) { - leftWidget = Container(constraints: widget.constraintsLeft!, child: leftWidget); + if (!(widget.constraintsLeft != null && widget.flexLeft != null)) { + if (widget.constraintsLeft != null) { + leftWidget = Container( + constraints: widget.constraintsLeft, + child: leftWidget, + ); + } + if (widget.flexLeft != null) { + leftWidget = Flexible(flex: widget.flexLeft!, child: leftWidget); + } } - if (widget.flexLeft != null) { - leftWidget = Flexible(flex: widget.flexLeft!, child: leftWidget); + + if (!(widget.constraintsRight != null && widget.flexRight != null)) { + if (widget.constraintsRight != null) { + rightWidget = Container( + constraints: widget.constraintsRight, + child: rightWidget, + ); + } + if (widget.flexRight != null) { + rightWidget = Flexible(flex: widget.flexRight!, child: rightWidget); + } } - } - if (!(widget.constraintsRight != null && widget.flexRight != null)) { - if (widget.constraintsRight != null) { - rightWidget = Container(constraints: widget.constraintsRight!, child: rightWidget); + if (widget.constraintsLeft != null && widget.flexLeft != null) { + leftWidget = ConstrainedWidthFlexible( + minWidth: widget.constraintsLeft?.minWidth ?? double.infinity, + maxWidth: widget.constraintsLeft?.maxWidth ?? double.infinity, + flex: widget.flexLeft!, + flexSum: widget.flexLeft! + (widget.flexRight ?? 0), + outerConstraints: constraints, + child: leftWidget, + ); } - if (widget.flexRight != null) { - rightWidget = Flexible(flex: widget.flexRight!, child: rightWidget); + + if (widget.constraintsRight != null && widget.flexRight != null) { + rightWidget = ConstrainedWidthFlexible( + minWidth: widget.constraintsRight?.minWidth ?? double.infinity, + maxWidth: widget.constraintsRight?.maxWidth ?? double.infinity, + flex: widget.flexRight!, + flexSum: widget.flexRight! + (widget.flexLeft ?? 0), + outerConstraints: constraints, + child: rightWidget, + ); } - } - - if (widget.constraintsLeft != null && widget.flexLeft != null) { - leftWidget = ConstrainedWidthFlexible( - minWidth: widget.constraintsLeft?.minWidth ?? double.infinity, - maxWidth: widget.constraintsLeft?.maxWidth ?? double.infinity, - flex: widget.flexLeft!, - flexSum: widget.flexLeft! + (widget.flexRight ?? 0), - outerConstraints: constraints, - child: leftWidget, - ); - } - - if (widget.constraintsRight != null && widget.flexRight != null) { - rightWidget = ConstrainedWidthFlexible( - minWidth: widget.constraintsRight?.minWidth ?? double.infinity, - maxWidth: widget.constraintsRight?.maxWidth ?? double.infinity, - flex: widget.flexRight!, - flexSum: widget.flexRight! + (widget.flexLeft ?? 0), - outerConstraints: constraints, - child: rightWidget, - ); - } - - Widget body = Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - leftWidget, - widget.dividerWidget ?? const SizedBox.shrink(), - rightWidget, - ], - ); - - if (widget.headerWidget != null) { - body = Column( - mainAxisAlignment: MainAxisAlignment.start, + + Widget body = Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - widget.headerWidget!, - body, + leftWidget, + widget.dividerWidget ?? const SizedBox.shrink(), + rightWidget, ], ); - } - return body; - }); + if (widget.headerWidget != null) { + body = Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + widget.headerWidget!, + body, + ], + ); + } + + return body; + }, + ); } } diff --git a/designer_v2/lib/common_views/mouse_events.dart b/designer_v2/lib/common_views/mouse_events.dart index d788e1676..6863caf3e 100644 --- a/designer_v2/lib/common_views/mouse_events.dart +++ b/designer_v2/lib/common_views/mouse_events.dart @@ -1,24 +1,28 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -typedef MouseEventsRegionBuilder = Widget Function(BuildContext context, Set state); +typedef MouseEventsRegionBuilder = Widget Function( + BuildContext context, + Set state, +); -typedef MaterialStatesChangedCallback = void Function(Set state); +typedef MaterialStatesChangedCallback = void Function(Set state); /// Helper widget that allows specifying both [onHover] and [onTap] callbacks /// for the widget it contains while exposing the current interaction state /// as a [WidgetInteractionState] to the child widget [builder] class MouseEventsRegion extends StatefulWidget { - const MouseEventsRegion( - {required this.builder, - this.onStateChanged, - this.onHover, - this.onTap, - this.onEnter, - this.onExit, - this.cursor = defaultCursor, - this.autoselectCursor = true, - super.key}); + const MouseEventsRegion({ + required this.builder, + this.onStateChanged, + this.onHover, + this.onTap, + this.onEnter, + this.onExit, + this.cursor = defaultCursor, + this.autoselectCursor = true, + super.key, + }); final MouseEventsRegionBuilder builder; final MaterialStatesChangedCallback? onStateChanged; @@ -47,12 +51,10 @@ class MouseEventsRegion extends StatefulWidget { } class _MouseEventsRegionState extends State { - late final MaterialStatesController statesController; + late final WidgetStatesController statesController; void handleStatesControllerChange() { - if (widget.onStateChanged != null) { - widget.onStateChanged!(statesController.value); - } + widget.onStateChanged?.call(statesController.value); // Force a rebuild to resolve MaterialStateProperty properties setState(() {}); } @@ -60,7 +62,7 @@ class _MouseEventsRegionState extends State { @override void initState() { super.initState(); - statesController = MaterialStatesController(); + statesController = WidgetStatesController(); statesController.addListener(handleStatesControllerChange); } @@ -68,29 +70,24 @@ class _MouseEventsRegionState extends State { Widget build(BuildContext context) { return GestureDetector( onTap: widget.onTap, - onTapDown: (_) => statesController.update(MaterialState.pressed, true), - onTapUp: (_) => statesController.update(MaterialState.pressed, false), - onTapCancel: () => statesController.update(MaterialState.pressed, false), + onTapDown: (_) => statesController.update(WidgetState.pressed, true), + onTapUp: (_) => statesController.update(WidgetState.pressed, false), + onTapCancel: () => statesController.update(WidgetState.pressed, false), child: MouseRegion( - cursor: widget.autoCursor, - onHover: (e) { - if (widget.onHover != null) { - widget.onHover!(e); - } - }, - onEnter: (e) { - statesController.update(MaterialState.hovered, true); - if (widget.onExit != null) { - widget.onEnter!(e); - } - }, - onExit: (e) { - statesController.update(MaterialState.hovered, false); - if (widget.onExit != null) { - widget.onExit!(e); - } - }, - child: widget.builder(context, statesController.value)), + cursor: widget.autoCursor, + onHover: (e) { + widget.onHover?.call(e); + }, + onEnter: (e) { + statesController.update(WidgetState.hovered, true); + widget.onEnter?.call(e); + }, + onExit: (e) { + statesController.update(WidgetState.hovered, false); + widget.onExit?.call(e); + }, + child: widget.builder(context, statesController.value), + ), ); } diff --git a/designer_v2/lib/common_views/navbar_tabbed.dart b/designer_v2/lib/common_views/navbar_tabbed.dart index 8d9a6c1a9..e54b77608 100644 --- a/designer_v2/lib/common_views/navbar_tabbed.dart +++ b/designer_v2/lib/common_views/navbar_tabbed.dart @@ -25,7 +25,10 @@ class NavbarTab { final bool enabled; } -typedef OnTabSelectCallback = void Function(int tabIdx, T tab); +typedef OnTabSelectCallback = void Function( + int tabIdx, + T tab, +); class TabbedNavbar extends ConsumerStatefulWidget { const TabbedNavbar({ @@ -59,15 +62,17 @@ class TabbedNavbar extends ConsumerStatefulWidget { final TabBarIndicatorSize? indicatorSize; final bool isScrollable; final Color? backgroundColor; - final MaterialStateProperty? overlayColor; + final WidgetStateProperty? overlayColor; final Color? labelColorHover; final Color? unselectedLabelColorHover; @override - ConsumerState createState() => _TabbedNavbarState(); + ConsumerState createState() => + _TabbedNavbarState(); } -class _TabbedNavbarState extends ConsumerState +class _TabbedNavbarState + extends ConsumerState with TickerProviderStateMixin implements Listenable { /// A [TabController] that has its index synced to the currently selected @@ -75,7 +80,8 @@ class _TabbedNavbarState extends ConsumerState _selectedTabIndex; set selectedTabIndex(int idx) { final tab = widget.tabs[idx]; @@ -116,7 +122,7 @@ class _TabbedNavbarState extends ConsumerState extends ConsumerState tabContent, - cursor: SystemMouseCursors.basic, ); if (widget.disabledTooltipText != null) { - return Tooltip(message: widget.disabledTooltipText!, child: disablePointerCursor); + return Tooltip( + message: widget.disabledTooltipText, + child: disablePointerCursor, + ); } return disablePointerCursor; } @@ -195,7 +203,7 @@ class _TabbedNavbarState extends ConsumerState extends ConsumerState extends ConsumerState _onSelectTab(t.index), // pass through on-tap event ), - MouseEventsRegion(builder: (context, states) { - // wrap spacer in mouse region to disable pointer mouse cursor - return Container( - width: widget.labelSpacing, - ); - }), + MouseEventsRegion( + builder: (context, states) { + // wrap spacer in mouse region to disable pointer mouse cursor + return Container( + width: widget.labelSpacing, + ); + }, + ), ], ), ); diff --git a/designer_v2/lib/common_views/pages/error_page.dart b/designer_v2/lib/common_views/pages/error_page.dart index 6666f42a0..d74f49fe1 100644 --- a/designer_v2/lib/common_views/pages/error_page.dart +++ b/designer_v2/lib/common_views/pages/error_page.dart @@ -17,7 +17,8 @@ class ErrorPage extends ConsumerWidget { children: [ SelectableText(error.toString()), TextButton( - onPressed: () => ref.read(routerProvider).dispatch(RoutingIntents.studies), + onPressed: () => + ref.read(routerProvider).dispatch(RoutingIntents.studies), child: Text(tr.navlink_error_home), ), ], diff --git a/designer_v2/lib/common_views/pages/splash_page.dart b/designer_v2/lib/common_views/pages/splash_page.dart index 88a8c41e8..64368c9a2 100644 --- a/designer_v2/lib/common_views/pages/splash_page.dart +++ b/designer_v2/lib/common_views/pages/splash_page.dart @@ -1,18 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -// import 'package:studyu_designer_v2/localization/app_translation.dart'; class SplashPage extends StatelessWidget { const SplashPage({super.key}); @override Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Text(AppLocalizations.of(context)!.loading_message), - // tr leads to unexpected null value, splash screen probably skips too fast - // child: Text(tr.loading_message), - ), - ); + return Container(); } } diff --git a/designer_v2/lib/common_views/primary_button.dart b/designer_v2/lib/common_views/primary_button.dart index 6fb9bc09d..49bae0478 100644 --- a/designer_v2/lib/common_views/primary_button.dart +++ b/designer_v2/lib/common_views/primary_button.dart @@ -12,7 +12,8 @@ class PrimaryButton extends StatefulWidget { this.onPressedFuture, this.enabled = true, this.showLoadingEarliestAfterMs = 100, - this.innerPadding = const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), + this.innerPadding = + const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), this.minimumSize, super.key, }); @@ -39,7 +40,8 @@ class PrimaryButton extends StatefulWidget { final EdgeInsets innerPadding; - bool get isDisabled => !enabled || (onPressed == null && onPressedFuture == null); + bool get isDisabled => + !enabled || (onPressed == null && onPressedFuture == null); final Size? minimumSize; @@ -48,7 +50,7 @@ class PrimaryButton extends StatefulWidget { } class _PrimaryButtonState extends State { - Future trackedFuture = Future.value(null); + Future trackedFuture = Future.value(); @override Widget build(BuildContext context) { @@ -59,19 +61,21 @@ class _PrimaryButtonState extends State { minimumSize: widget.minimumSize, ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)); - final tooltipMessage = (!widget.isDisabled) ? widget.tooltip : widget.tooltipDisabled; + final tooltipMessage = + (!widget.isDisabled) ? widget.tooltip : widget.tooltipDisabled; - onButtonPressed() { + void onButtonPressed() { widget.onPressed?.call(); if (widget.onPressedFuture != null) { final future = widget.onPressedFuture!().whenComplete(() { if (mounted) { setState(() { - trackedFuture = Future.value(null); + trackedFuture = Future.value(); }); } }); - Future.delayed(Duration(milliseconds: widget.showLoadingEarliestAfterMs), () { + Future.delayed( + Duration(milliseconds: widget.showLoadingEarliestAfterMs), () { if (mounted) { setState(() { trackedFuture = future; @@ -92,16 +96,17 @@ class _PrimaryButtonState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - widget.isLoading - ? SizedBox( - width: theme.iconTheme.size ?? 14.0, - height: theme.iconTheme.size ?? 14.0, - child: const CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2.0, - ), - ) - : Icon(widget.icon), + if (widget.isLoading) + SizedBox( + width: theme.iconTheme.size ?? 14.0, + height: theme.iconTheme.size ?? 14.0, + child: const CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2.0, + ), + ) + else + Icon(widget.icon), const SizedBox(width: 6.0), Text(widget.text, textAlign: TextAlign.center), ], @@ -112,23 +117,24 @@ class _PrimaryButtonState extends State { } return Tooltip( - message: tooltipMessage, - child: ElevatedButton( - style: primaryStyle, - onPressed: widget.isDisabled ? null : onButtonPressed, - child: Padding( - padding: widget.innerPadding, - child: widget.isLoading - ? SizedBox( - width: theme.iconTheme.size ?? 14.0, - height: theme.iconTheme.size ?? 14.0, - child: const CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2.0, - ), - ) - : Text(widget.text, textAlign: TextAlign.center), - ), - )); + message: tooltipMessage, + child: ElevatedButton( + style: primaryStyle, + onPressed: widget.isDisabled ? null : onButtonPressed, + child: Padding( + padding: widget.innerPadding, + child: widget.isLoading + ? SizedBox( + width: theme.iconTheme.size ?? 14.0, + height: theme.iconTheme.size ?? 14.0, + child: const CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2.0, + ), + ) + : Text(widget.text, textAlign: TextAlign.center), + ), + ), + ); } } diff --git a/designer_v2/lib/common_views/range_slider.dart b/designer_v2/lib/common_views/range_slider.dart index 8d484a08a..e0ea2bd3f 100644 --- a/designer_v2/lib/common_views/range_slider.dart +++ b/designer_v2/lib/common_views/range_slider.dart @@ -31,7 +31,11 @@ class IndicatorRangeSliderThumbShape extends RangeSliderThumbShape { if (thumb == null) return; final Canvas canvas = context.canvas; - canvas.drawCircle(center, 9, Paint()..color = Theme.of(buildContext).colorScheme.primary); + canvas.drawCircle( + center, + 9, + Paint()..color = Theme.of(buildContext).colorScheme.primary, + ); final value = thumb == Thumb.start ? start : end; // Customize the box style @@ -48,8 +52,12 @@ class IndicatorRangeSliderThumbShape extends RangeSliderThumbShape { // Calculate text size and position final text = value.toString(); - final textSpan = TextSpan(text: text, style: const TextStyle(color: Colors.white)); - final textPainter = TextPainter(text: textSpan, textDirection: textDirection ?? TextDirection.ltr); + final textSpan = + TextSpan(text: text, style: const TextStyle(color: Colors.white)); + final textPainter = TextPainter( + text: textSpan, + textDirection: textDirection ?? TextDirection.ltr, + ); textPainter.layout(); // Calculate the box size with padding @@ -60,10 +68,18 @@ class IndicatorRangeSliderThumbShape extends RangeSliderThumbShape { final textOffset = center + Offset(-textPainter.width / 2, 15); // Draw the rounded rectangle box with border centered around the text - final boxOffset = textOffset + Offset(textPainter.width / 2 - boxWidth / 2, textPainter.height / 2 - boxHeight / 2); - final boxRect = Rect.fromLTWH(boxOffset.dx, boxOffset.dy, boxWidth, boxHeight); + final boxOffset = textOffset + + Offset( + textPainter.width / 2 - boxWidth / 2, + textPainter.height / 2 - boxHeight / 2, + ); + final boxRect = + Rect.fromLTWH(boxOffset.dx, boxOffset.dy, boxWidth, boxHeight); canvas.drawRRect(RRect.fromRectAndRadius(boxRect, borderRadius), boxPaint); - canvas.drawRRect(RRect.fromRectAndRadius(boxRect, borderRadius), borderPaint); + canvas.drawRRect( + RRect.fromRectAndRadius(boxRect, borderRadius), + borderPaint, + ); // Draw the text inside the box textPainter.paint(canvas, textOffset); diff --git a/designer_v2/lib/common_views/reactive_color_picker.dart b/designer_v2/lib/common_views/reactive_color_picker.dart index c03f79245..5e949711e 100644 --- a/designer_v2/lib/common_views/reactive_color_picker.dart +++ b/designer_v2/lib/common_views/reactive_color_picker.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:reactive_forms/reactive_forms.dart'; - import 'package:flutter_colorpicker/flutter_colorpicker.dart'; +import 'package:reactive_forms/reactive_forms.dart'; import 'package:studyu_designer_v2/utils/color.dart'; /// Adaption of [ReactiveColorPicker] from 'reactive_color_picker' package @@ -38,25 +37,31 @@ class ReactiveCustomColorPicker extends ReactiveFormField { super.formControlName, super.formControl, super.validationMessages, - ControlValueAccessor? valueAccessor, - ShowErrorsFunction? showErrors, //////////////////////////////////////////////////////////////////////////// Color? contrastIconColorLight, Color contrastIconColorDark = Colors.white, InputDecoration? decoration, PaletteType paletteType = PaletteType.hsv, bool enableAlpha = true, - @Deprecated('Use empty list in [labelTypes] to disable label.') bool showLabel = true, - @Deprecated('Use Theme.of(context).textTheme.bodyText1 & 2 to alter text style.') TextStyle? labelTextStyle, + @Deprecated('Use empty list in [labelTypes] to disable label.') + bool showLabel = true, + @Deprecated( + 'Use Theme.of(context).textTheme.bodyText1 & 2 to alter text style.', + ) + TextStyle? labelTextStyle, bool displayThumbColor = false, bool portraitOnly = false, bool hexInputBar = false, double colorPickerWidth = 300.0, double pickerAreaHeightPercent = 1.0, - BorderRadius pickerAreaBorderRadius = const BorderRadius.all(Radius.zero), + BorderRadius pickerAreaBorderRadius = BorderRadius.zero, double disabledOpacity = 0.5, HSVColor? pickerHsvColor, - List labelTypes = const [ColorLabelType.rgb, ColorLabelType.hsv, ColorLabelType.hsl], + List labelTypes = const [ + ColorLabelType.rgb, + ColorLabelType.hsv, + ColorLabelType.hsl, + ], TextEditingController? hexInputController, List? colorHistory, ValueChanged>? onHistoryChanged, @@ -71,8 +76,8 @@ class ReactiveCustomColorPicker extends ReactiveFormField { context: context, builder: (BuildContext context) { return AlertDialog( - titlePadding: const EdgeInsets.all(0.0), - contentPadding: const EdgeInsets.all(0.0), + titlePadding: EdgeInsets.zero, + contentPadding: EdgeInsets.zero, content: SingleChildScrollView( child: ColorPicker( pickerColor: pickerColor, @@ -97,13 +102,16 @@ class ReactiveCustomColorPicker extends ReactiveFormField { ); } - final isEmptyValue = field.value == null || field.value.toString().isEmpty; + final isEmptyValue = + field.value == null || field.value.toString().isEmpty; - final InputDecoration effectiveDecoration = - (decoration ?? const InputDecoration()).applyDefaults(Theme.of(field.context).inputDecorationTheme); + final InputDecoration effectiveDecoration = (decoration ?? + const InputDecoration()) + .applyDefaults(Theme.of(field.context).inputDecorationTheme); - final iconColor = - (field.value?.computeLuminance() ?? 0) < 0.2 ? contrastIconColorDark : contrastIconColorLight; + final iconColor = (field.value?.computeLuminance() ?? 0) < 0.2 + ? contrastIconColorDark + : contrastIconColorLight; return IgnorePointer( ignoring: !field.control.enabled, @@ -131,7 +139,9 @@ class ReactiveCustomColorPicker extends ReactiveFormField { // Convert between non-serializable [Color] // superclass that color_picker expects & // the [SerializableColor] type of the control - field.didChange(SerializableColor(color.value)); + field.didChange( + SerializableColor(color.value), + ); }, ); }, @@ -149,7 +159,8 @@ class ReactiveCustomColorPicker extends ReactiveFormField { ], ), ), - isEmpty: isEmptyValue && effectiveDecoration.hintText == null, + isEmpty: + isEmptyValue && effectiveDecoration.hintText == null, child: Container( color: field.value, ), diff --git a/designer_v2/lib/common_views/search.dart b/designer_v2/lib/common_views/search.dart index ece8b9b8f..b77130b32 100644 --- a/designer_v2/lib/common_views/search.dart +++ b/designer_v2/lib/common_views/search.dart @@ -35,7 +35,7 @@ class SearchState extends State { } void _onSearchPressed() { - String query = _searchController.text.toLowerCase(); + final String query = _searchController.text.toLowerCase(); widget.onQueryChanged(query); } @@ -51,7 +51,7 @@ class SearchState extends State { hintText: widget.hintText ?? "Search", controller: _searchController, leading: const Icon(Icons.search), - shadowColor: MaterialStateProperty.resolveWith((states) { + shadowColor: WidgetStateProperty.resolveWith((states) { return Colors.transparent; }), ); diff --git a/designer_v2/lib/common_views/secondary_button.dart b/designer_v2/lib/common_views/secondary_button.dart index e61cbdb6e..8d6372003 100644 --- a/designer_v2/lib/common_views/secondary_button.dart +++ b/designer_v2/lib/common_views/secondary_button.dart @@ -41,7 +41,9 @@ class SecondaryButton extends StatelessWidget { return OutlinedButton( style: secondaryStyle, onPressed: onPressed, - child: isLoading ? const CircularProgressIndicator() : Text(text, textAlign: TextAlign.center), + child: isLoading + ? const CircularProgressIndicator() + : Text(text, textAlign: TextAlign.center), ); } } diff --git a/designer_v2/lib/common_views/sidesheet/sidesheet.dart b/designer_v2/lib/common_views/sidesheet/sidesheet.dart index 51846da7e..2c258073a 100644 --- a/designer_v2/lib/common_views/sidesheet/sidesheet.dart +++ b/designer_v2/lib/common_views/sidesheet/sidesheet.dart @@ -27,12 +27,18 @@ class Sidesheet extends StatefulWidget { this.withCloseButton = false, this.ignoreAppBar = true, this.collapseSingleTab = false, - this.bodyPadding = const EdgeInsets.symmetric(vertical: 32.0, horizontal: 48.0), + this.bodyPadding = + const EdgeInsets.symmetric(vertical: 32.0, horizontal: 48.0), this.wrapContent, super.key, - }) : assert((body != null && tabs == null) || (body == null && tabs != null), - "Must provide either body or tabs to build sidesheet content"), - assert(tabs == null || tabs.length >= 1, "Must provide at least one tab to build sidesheet content"); + }) : assert( + (body != null && tabs == null) || (body == null && tabs != null), + "Must provide either body or tabs to build sidesheet content", + ), + assert( + tabs == null || tabs.length >= 1, + "Must provide at least one tab to build sidesheet content", + ); final String titleText; final Widget? body; @@ -78,18 +84,17 @@ class _SidesheetState extends State { : 0; final actualHeight = screen.size.height - exceptionalHeight; - final backgroundColor = ThemeConfig.sidesheetBackgroundColor(Theme.of(context)); + final backgroundColor = + ThemeConfig.sidesheetBackgroundColor(Theme.of(context)); return Align( alignment: Alignment.bottomLeft, child: Material( - elevation: 0, color: Colors.white, child: SizedBox( width: actualWidth, height: actualHeight, child: Scaffold( - appBar: null, backgroundColor: backgroundColor, body: widget.withCloseButton ? Stack( @@ -99,7 +104,7 @@ class _SidesheetState extends State { top: 5, right: 5, child: CloseButton(), - ) + ), ], ) : _build(context, widget.body, widget.tabs), @@ -109,12 +114,18 @@ class _SidesheetState extends State { ); } - _build(BuildContext context, Widget? body, List? tabs) { + Container _build( + BuildContext context, + Widget? body, + List? tabs, + ) { final theme = Theme.of(context); - final backgroundColor = ThemeConfig.sidesheetBackgroundColor(Theme.of(context)); + final backgroundColor = + ThemeConfig.sidesheetBackgroundColor(Theme.of(context)); final hasTabs = tabs != null; - final isCollapsed = tabs != null && tabs.length == 1 && widget.collapseSingleTab; + final isCollapsed = + tabs != null && tabs.length == 1 && widget.collapseSingleTab; final innerBody = (body != null) ? body @@ -122,22 +133,24 @@ class _SidesheetState extends State { ? selectedTab!.builder(context) : const SizedBox.shrink(); - final actualWrapContent = widget.wrapContent ?? (widget) => widget; // default to identity no-op + final actualWrapContent = + widget.wrapContent ?? (widget) => widget; // default to identity no-op final tabBarLabelHoverColor = - (theme.tabBarTheme.labelColor ?? theme.tabBarTheme.labelStyle?.color)?.faded(ThemeConfig.kHoverFadeFactor); + (theme.tabBarTheme.labelColor ?? theme.tabBarTheme.labelStyle?.color) + ?.faded(ThemeConfig.kHoverFadeFactor); return Container( decoration: BoxDecoration( border: Border( left: BorderSide( - color: (theme.dividerTheme.color ?? theme.dividerColor).withOpacity(0.1), + color: (theme.dividerTheme.color ?? theme.dividerColor) + .withOpacity(0.1), ), ), ), child: actualWrapContent( Column( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( @@ -152,42 +165,51 @@ class _SidesheetState extends State { children: [ SelectableText( widget.titleText, - style: theme.textTheme.headlineSmall!.copyWith(fontWeight: FontWeight.normal), + style: theme.textTheme.headlineSmall! + .copyWith(fontWeight: FontWeight.normal), ), - (widget.actionButtons != null) - ? Wrap(spacing: 8.0, children: widget.actionButtons!) - : const SizedBox.shrink(), + if (widget.actionButtons != null) + Wrap(spacing: 8.0, children: widget.actionButtons!) + else + const SizedBox.shrink(), ], ), ), - (hasTabs && !isCollapsed) - ? Padding( - padding: EdgeInsets.symmetric(horizontal: (widget.bodyPadding?.horizontal ?? 0) * 0.5), - child: TabbedNavbar( - tabs: tabs, - height: 38.0, - onSelect: _onTabChange, - labelPadding: EdgeInsets.zero, - labelSpacing: 24.0, - isScrollable: true, - indicatorSize: TabBarIndicatorSize.label, - backgroundColor: backgroundColor, - overlayColor: MaterialStateColor.resolveWith( - (states) => Colors.transparent, - ), - labelColorHover: tabBarLabelHoverColor, - unselectedLabelColorHover: tabBarLabelHoverColor, - ), - ) - : const SizedBox.shrink(), - (hasTabs && !isCollapsed) ? const Divider(height: 1) : const Divider(), + if (hasTabs && !isCollapsed) + Padding( + padding: EdgeInsets.symmetric( + horizontal: (widget.bodyPadding?.horizontal ?? 0) * 0.5, + ), + child: TabbedNavbar( + tabs: tabs, + height: 38.0, + onSelect: _onTabChange, + labelPadding: EdgeInsets.zero, + labelSpacing: 24.0, + isScrollable: true, + indicatorSize: TabBarIndicatorSize.label, + backgroundColor: backgroundColor, + overlayColor: WidgetStateColor.resolveWith( + (states) => Colors.transparent, + ), + labelColorHover: tabBarLabelHoverColor, + unselectedLabelColorHover: tabBarLabelHoverColor, + ), + ) + else + const SizedBox.shrink(), + if (hasTabs && !isCollapsed) + const Divider(height: 1) + else + const Divider(), Flexible( child: SingleChildScrollView( child: Column( children: [ - (hasTabs && !isCollapsed) // compensate for divider height loss - ? const SizedBox(height: 12.0) - : const SizedBox.shrink(), + if (hasTabs && !isCollapsed) + const SizedBox(height: 12.0) + else + const SizedBox.shrink(), Padding( padding: EdgeInsets.fromLTRB( (widget.bodyPadding?.horizontal ?? 0) * 0.5, @@ -196,7 +218,7 @@ class _SidesheetState extends State { (widget.bodyPadding?.vertical ?? 0) * 0.5, ), child: innerBody, - ) + ), ], ), ), @@ -228,7 +250,8 @@ Future showModalSideSheet({ assert(!barrierDismissible || barrierLabel != null); return showGeneralDialog( barrierDismissible: barrierDismissible, - barrierColor: barrierColor ?? ThemeConfig.modalBarrierColor(Theme.of(context)), + barrierColor: + barrierColor ?? ThemeConfig.modalBarrierColor(Theme.of(context)), transitionDuration: transitionDuration, barrierLabel: barrierLabel, useRootNavigator: useRootNavigator, @@ -246,7 +269,8 @@ Future showModalSideSheet({ ), transitionBuilder: (_, animation, __, child) { return SlideTransition( - position: Tween(begin: const Offset(-1, 0), end: Offset.zero).animate(animation), + position: Tween(begin: const Offset(-1, 0), end: Offset.zero) + .animate(animation), child: child, ); }, diff --git a/designer_v2/lib/common_views/sidesheet/sidesheet_form.dart b/designer_v2/lib/common_views/sidesheet/sidesheet_form.dart index de3fed73f..c74cc5bba 100644 --- a/designer_v2/lib/common_views/sidesheet/sidesheet_form.dart +++ b/designer_v2/lib/common_views/sidesheet/sidesheet_form.dart @@ -20,7 +20,7 @@ class FormSideSheetTab extends NavbarTab { FormViewBuilder formViewBuilder; } -showFormSideSheet({ +Future showFormSideSheet({ required BuildContext context, required T formViewModel, FormViewBuilder? formViewBuilder, @@ -35,7 +35,7 @@ showFormSideSheet({ barrierColor ??= ThemeConfig.modalBarrierColor(Theme.of(context)); // Wraps the whole side sheet in a [ReactiveForm] widget - Widget wrapInForm(widget) { + Widget wrapInForm(Widget widget) { return ReactiveForm( formGroup: formViewModel.form, child: widget, @@ -44,12 +44,14 @@ showFormSideSheet({ // Bind the [formViewModel] to the [SidesheetTab]s' widget builder final List? boundTabs = tabs - ?.map((t) => SidesheetTab( - title: t.title, - index: t.index, - enabled: t.enabled, - builder: (BuildContext context) => t.formViewBuilder(formViewModel), - )) + ?.map( + (t) => SidesheetTab( + title: t.title, + index: t.index, + enabled: t.enabled, + builder: (BuildContext context) => t.formViewBuilder(formViewModel), + ), + ) .toList(); return showModalSideSheet( diff --git a/designer_v2/lib/common_views/standard_table.dart b/designer_v2/lib/common_views/standard_table.dart index 1acbe5a66..4736e0089 100644 --- a/designer_v2/lib/common_views/standard_table.dart +++ b/designer_v2/lib/common_views/standard_table.dart @@ -11,10 +11,17 @@ import 'package:studyu_designer_v2/utils/model_action.dart'; typedef OnSelectHandler = void Function(T item); -typedef StandardTableRowBuilder = TableRow Function(BuildContext context, List columns); +typedef StandardTableRowBuilder = TableRow Function( + BuildContext context, + List columns, +); typedef StandardTableCellsBuilder = List Function( - BuildContext context, T item, int rowIdx, Set states); + BuildContext context, + T item, + int rowIdx, + Set states, +); enum StandardTableStyle { plain, material } @@ -66,7 +73,10 @@ class StandardTable extends StatefulWidget { }) { if (trailingActionsColumn == null) { this.inputTrailingActionsColumn = StandardTableColumn( - label: '', columnWidth: const MaxColumnWidth(IntrinsicColumnWidth(), FixedColumnWidth(65))); + label: '', + columnWidth: + const MaxColumnWidth(IntrinsicColumnWidth(), FixedColumnWidth(65)), + ); } else { this.inputTrailingActionsColumn = trailingActionsColumn; } @@ -124,9 +134,9 @@ class _StandardTableState extends State> { /// Cached list of [TableRow]s corresponding to each item in [widget.items] final List _cachedRows = []; - /// Current set of [MaterialState]s for each row in [_cachedRows] + /// Current set of [WidgetState]s for each row in [_cachedRows] /// Used to keep track of current hover & pressed status - final List> _rowStates = []; + final List> _rowStates = []; /// Indices to rebuild [TableRow]s for instead of using the cached version final Set _dirtyRowIndices = {}; @@ -150,14 +160,14 @@ class _StandardTableState extends State> { super.didUpdateWidget(oldWidget); } - _initRowStates() { + void _initRowStates() { _rowStates.clear(); - for (var _ in widget.items) { - _rowStates.add({}); + for (final _ in widget.items) { + _rowStates.add({}); } } - _onRowStateChanged(int rowIdx, Set states) { + void _onRowStateChanged(int rowIdx, Set states) { setState(() { _rowStates[rowIdx] = states; // flag row for rebuild to reflect its current set of [MaterialStatus] @@ -175,20 +185,24 @@ class _StandardTableState extends State> { } final headerRow = _buildHeaderRow(); - final tableHeaderRows = (widget.showTableHeader) ? [headerRow, paddingRow, paddingRow] : []; + final List tableHeaderRows = + (widget.showTableHeader) ? [headerRow, paddingRow, paddingRow] : []; final tableDataRows = _tableRows(theme); Widget tableWidget = Table( - columnWidths: columnWidths, - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: [...tableHeaderRows, ...tableDataRows]); + columnWidths: columnWidths, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [...tableHeaderRows, ...tableDataRows], + ); if (widget.tableWrapper != null) { tableWidget = widget.tableWrapper!(tableWidget); } final isTableVisible = !(tableHeaderRows.isEmpty && tableDataRows.isEmpty); - if (tableDataRows.isEmpty && widget.emptyWidget != null && widget.hideLeadingTrailingWhenEmpty) { + if (tableDataRows.isEmpty && + widget.emptyWidget != null && + widget.hideLeadingTrailingWhenEmpty) { return widget.emptyWidget!; } @@ -197,16 +211,23 @@ class _StandardTableState extends State> { crossAxisAlignment: CrossAxisAlignment.start, children: [ widget.leadingWidget ?? const SizedBox.shrink(), - (widget.leadingWidget != null && widget.leadingWidgetSpacing != null) - ? SizedBox(height: widget.leadingWidgetSpacing!) - : const SizedBox.shrink(), - (isTableVisible) ? tableWidget : Container(), - (!isTableVisible && widget.emptyWidget != null && !widget.hideLeadingTrailingWhenEmpty) - ? widget.emptyWidget! - : const SizedBox.shrink(), - (widget.trailingWidget != null && widget.trailingWidgetSpacing != null) - ? SizedBox(height: widget.trailingWidgetSpacing!) - : const SizedBox.shrink(), + if (widget.leadingWidget != null && + widget.leadingWidgetSpacing != null) + SizedBox(height: widget.leadingWidgetSpacing) + else + const SizedBox.shrink(), + if (isTableVisible) tableWidget else Container(), + if (!isTableVisible && + widget.emptyWidget != null && + !widget.hideLeadingTrailingWhenEmpty) + widget.emptyWidget! + else + const SizedBox.shrink(), + if (widget.trailingWidget != null && + widget.trailingWidgetSpacing != null) + SizedBox(height: widget.trailingWidgetSpacing) + else + const SizedBox.shrink(), widget.trailingWidget ?? const SizedBox.shrink(), ], ); @@ -219,14 +240,28 @@ class _StandardTableState extends State> { final sortAscending = widget.inputColumns[columnIndex].sortAscending; // Save default order to restore later - if (sortDefaultOrder == null || sortDefaultOrder!.length != widget.items.length) { + if (sortDefaultOrder == null || + sortDefaultOrder!.length != widget.items.length) { sortDefaultOrder = List.from(widget.items); } if (sortAscending != null) { widget.items.sort((a, b) { - return _sortLogic(a, b, columnIndex: columnIndex, sortAscending: sortAscending); + return _sortLogic( + a, + b, + columnIndex: columnIndex, + sortAscending: sortAscending, + ); }); +<<<<<<< HEAD +======= + _sortPinnedStudies( + widget.items, + columnIndex: columnIndex, + sortAscending: sortAscending, + ); +>>>>>>> dev } else { widget.items.clear(); widget.items.addAll(sortDefaultOrder!); @@ -234,16 +269,70 @@ class _StandardTableState extends State> { _cachedRows.clear(); } +<<<<<<< HEAD int _sortLogic(T a, T b, {required int columnIndex, required bool? sortAscending}) { final sortPredicate = widget.sortColumnPredicates; if (sortPredicate != null && sortPredicate[columnIndex] != null) { +======= + void _sortPinnedStudies( + List items, { + required int columnIndex, + bool? sortAscending, + }) { + // Extract and insert pinned items at the top + if (widget.pinnedPredicates != null) { + items.sort((a, b) { + final int ret = widget.pinnedPredicates!(a, b); + // Fallback to default sorting algorithm + return ret == 0 + ? _sortLogic( + a, + b, + columnIndex: columnIndex, + sortAscending: sortAscending, + ) + : ret; + }); + } + } + + int _sortLogic( + T a, + T b, { + required int columnIndex, + required bool? sortAscending, + bool? useSortPredicate, + }) { + final sortPredicate = widget.sortColumnPredicates; + if (useSortPredicate != null && + useSortPredicate && + sortPredicate != null && + sortPredicate[columnIndex] != null) { + final int res; +>>>>>>> dev if (sortAscending ?? true) { return sortPredicate[columnIndex]!(a, b); } +<<<<<<< HEAD return sortPredicate[columnIndex]!(b, a); +======= + if (res == 0) { + // Fallback to default sorting algorithm + return _sortLogic( + a, + b, + columnIndex: columnIndex, + sortAscending: sortAscending, + useSortPredicate: false, + ); + } + return res; +>>>>>>> dev } else if (a is Comparable && b is Comparable) { // If sortPredicate is not provided, use default comparison logic - return sortAscending ?? true ? Comparable.compare(a, b) : Comparable.compare(b, a); + return sortAscending ?? true + ? Comparable.compare(a, b) + : Comparable.compare(b, a); } else { return 0; } @@ -262,10 +351,13 @@ class _StandardTableState extends State> { _cachedRows.addAll(rows); // Add padding after each row - return rows.map((dataRow) => [dataRow, paddingRow]).expand((element) => element).toList(); + return rows + .map((dataRow) => [dataRow, paddingRow]) + .expand((element) => element) + .toList(); } - TableRow _useCachedOrRebuildRow(rowIdx) { + TableRow _useCachedOrRebuildRow(int rowIdx) { if (rowIdx >= _cachedRows.length) { // [_cachedRows] is empty when building for the first time return _buildDataRow(rowIdx); @@ -279,8 +371,11 @@ class _StandardTableState extends State> { } TableRow _buildPaddingRow() { - TableRow rowSpacer = - TableRow(children: widget.inputColumns.map((_) => SizedBox(height: widget.rowSpacing)).toList()); + final TableRow rowSpacer = TableRow( + children: widget.inputColumns + .map((_) => SizedBox(height: widget.rowSpacing)) + .toList(), + ); return rowSpacer; } @@ -289,21 +384,30 @@ class _StandardTableState extends State> { return headerRowBuilder(context, widget.inputColumns); } - TableRow _defaultHeader(BuildContext context, List columns) { + TableRow _defaultHeader( + BuildContext context, + List columns, + ) { final theme = Theme.of(context); final List headerCells = []; for (var i = 0; i < columns.length; i++) { final isLeading = i == 0; final isTrailing = i == columns.length - 1; - headerCells.add(MouseEventsRegion( - builder: (context, state) { - return Padding( + headerCells.add( + MouseEventsRegion( + builder: (context, state) { + return Padding( padding: EdgeInsets.fromLTRB( - (isLeading || isTrailing) ? 2 * widget.cellSpacing : widget.cellSpacing, - widget.cellSpacing, - (isLeading || isTrailing) ? 2 * widget.cellSpacing : widget.cellSpacing, - widget.cellSpacing), + (isLeading || isTrailing) + ? 2 * widget.cellSpacing + : widget.cellSpacing, + widget.cellSpacing, + (isLeading || isTrailing) + ? 2 * widget.cellSpacing + : widget.cellSpacing, + widget.cellSpacing, + ), child: Row( children: [ Text( @@ -315,16 +419,20 @@ class _StandardTableState extends State> { color: theme.colorScheme.onSurface.withOpacity(0.8), ), ), - widget.inputColumns[i].sortable - ? widget.inputColumns[i].sortableIcon ?? const SizedBox(width: 17) - : const SizedBox.shrink(), + if (widget.inputColumns[i].sortable) + widget.inputColumns[i].sortableIcon ?? + const SizedBox(width: 17) + else + const SizedBox.shrink(), ], - )); - }, - onEnter: (event) => setState(() => sortAction(i, hover: event)), - onExit: (event) => setState(() => sortAction(i, hover: event)), - onTap: () => setState(() => sortAction(i)), - )); + ), + ); + }, + onEnter: (event) => setState(() => sortAction(i, hover: event)), + onExit: (event) => setState(() => sortAction(i, hover: event)), + onTap: () => setState(() => sortAction(i)), + ), + ); } return TableRow(children: headerCells); } @@ -342,21 +450,18 @@ class _StandardTableState extends State> { switch (widget.inputColumns[i].sortAscending) { case null: // Reset all column sorting - for (StandardTableColumn c in widget.inputColumns) { + for (final StandardTableColumn c in widget.inputColumns) { c.sortAscending = null; c.sortableIcon = null; } widget.inputColumns[i].sortAscending = true; widget.inputColumns[i].sortableIcon = ascendingIcon; - break; case true: widget.inputColumns[i].sortAscending = false; widget.inputColumns[i].sortableIcon = descendingIcon; - break; case false: widget.inputColumns[i].sortAscending = null; widget.inputColumns[i].sortableIcon = null; - break; } _sortColumn(i); // No sorting icon is active or hovered @@ -372,27 +477,29 @@ class _StandardTableState extends State> { TableRow _buildDataRow(int rowIdx) { final item = widget.items[rowIdx]; - final dataRowBuilder = widget.dataRowBuilder ?? _defaultDataRow; final rowStates = _rowStates[rowIdx]; - return dataRowBuilder(context, item, rowIdx, rowStates); + return widget.dataRowBuilder != null + ? widget.dataRowBuilder!(context, widget.inputColumns) + : _defaultDataRow(context, item, rowIdx, rowStates); } TableRow _defaultDataRow( BuildContext context, T item, int rowIdx, - Set states, + Set states, ) { final theme = Theme.of(context); - final rowIsHovered = states.contains(MaterialState.hovered); - final rowIsPressed = states.contains(MaterialState.pressed); - final rowColor = - (widget.rowStyle == StandardTableStyle.material) ? theme.colorScheme.onPrimary : Colors.transparent; + final rowIsHovered = states.contains(WidgetState.hovered); + final rowIsPressed = states.contains(WidgetState.pressed); + final rowColor = (widget.rowStyle == StandardTableStyle.material) + ? theme.colorScheme.onPrimary + : Colors.transparent; - Widget decorateCellInteractions(Widget child, {disableOnTap = false}) { + Widget decorateCellInteractions(Widget child, {bool disableOnTap = false}) { return MouseEventsRegion( - onTap: (disableOnTap) ? null : (() => widget.onSelectItem(item)), + onTap: disableOnTap ? null : (() => widget.onSelectItem(item)), onStateChanged: (states) => _onRowStateChanged(rowIdx, states), builder: (context, mouseEventState) => child, ); @@ -400,48 +507,57 @@ class _StandardTableState extends State> { Widget decorateCell( Widget child, { - alignment = Alignment.centerLeft, - isLeading = false, - isTrailing = false, - disableOnTap = false, + Alignment alignment = Alignment.centerLeft, + bool isLeading = false, + bool isTrailing = false, + bool disableOnTap = false, }) { final content = Align( alignment: alignment, child: child, ); final styledCell = Material( - color: rowColor, - child: Padding( - padding: EdgeInsets.fromLTRB( - (isLeading || isTrailing) ? 2 * widget.cellSpacing : widget.cellSpacing, - widget.cellSpacing, - (isLeading || isTrailing) ? 2 * widget.cellSpacing : widget.cellSpacing, - widget.cellSpacing), - child: (widget.minRowHeight != null) - ? SizedBox( - height: widget.minRowHeight, - child: content, - ) - : content, - )); + color: rowColor, + child: Padding( + padding: EdgeInsets.fromLTRB( + (isLeading || isTrailing) + ? 2 * widget.cellSpacing + : widget.cellSpacing, + widget.cellSpacing, + (isLeading || isTrailing) + ? 2 * widget.cellSpacing + : widget.cellSpacing, + widget.cellSpacing, + ), + child: (widget.minRowHeight != null) + ? SizedBox( + height: widget.minRowHeight, + child: content, + ) + : content, + ), + ); return (!widget.disableRowInteractions) ? decorateCellInteractions(styledCell, disableOnTap: disableOnTap) : styledCell; } - Widget applyColumnConfiguration(Widget cellWidget, StandardTableColumn column) { + Widget applyColumnConfiguration( + Widget cellWidget, + StandardTableColumn column, + ) { if (column.tooltip != null) { - cellWidget = Tooltip( - message: column.tooltip!, + return Tooltip( + message: column.tooltip, child: cellWidget, ); } - return cellWidget; } - final List rawCells = widget.buildCellsAt(context, item, rowIdx, states); + final List rawCells = + widget.buildCellsAt(context, item, rowIdx, states); if (widget.trailingActionsAt != null) { // Insert additional table cell to hold actions menu @@ -462,7 +578,6 @@ class _StandardTableState extends State> { cell, isLeading: isLeading, isTrailing: isTrailing, - disableOnTap: false, ); cell = applyColumnConfiguration(cell, cellColumnConfig); dataCells.add(cell); @@ -474,21 +589,22 @@ class _StandardTableState extends State> { children: dataCells, decoration: BoxDecoration( border: Border.all( - color: (rowIsPressed) + color: rowIsPressed ? theme.colorScheme.primary.withOpacity(0.7) : theme.colorScheme.primaryContainer.withOpacity(0.9), ), borderRadius: const BorderRadius.all(Radius.circular(4)), boxShadow: [ BoxShadow( - color: (rowIsPressed) - ? theme.colorScheme.primary.withOpacity(0.15) - : ((rowIsHovered) - ? theme.colorScheme.onSurface.withOpacity(0.2) - : theme.colorScheme.onSurface.withOpacity(0.1)), - spreadRadius: 0, - blurRadius: (rowIsHovered) ? 3 : 2, - offset: (rowIsHovered) ? const Offset(1, 1) : const Offset(0, 1)) + color: rowIsPressed + ? theme.colorScheme.primary.withOpacity(0.15) + : (rowIsHovered + ? theme.colorScheme.onSurface.withOpacity(0.2) + : theme.colorScheme.onSurface.withOpacity(0.1)), + blurRadius: rowIsHovered ? 3 : 2, + offset: + rowIsHovered ? const Offset(1, 1) : const Offset(0, 1), + ), ], color: theme.colorScheme.onPrimary, ), @@ -496,7 +612,6 @@ class _StandardTableState extends State> { : TableRow( key: ObjectKey(item), children: dataCells, - decoration: null, ); } @@ -511,7 +626,6 @@ class _StandardTableState extends State> { final theme = Theme.of(context); actionMenuWidget = ActionPopUpMenuButton( actions: actions, - orientation: Axis.horizontal, triggerIconColor: ThemeConfig.bodyTextMuted(theme).color?.faded(0.6), triggerIconColorHover: theme.colorScheme.primary, disableSplashEffect: true, diff --git a/designer_v2/lib/common_views/studyu_logo.dart b/designer_v2/lib/common_views/studyu_logo.dart index 077265402..810f14e2e 100644 --- a/designer_v2/lib/common_views/studyu_logo.dart +++ b/designer_v2/lib/common_views/studyu_logo.dart @@ -10,21 +10,25 @@ class StudyULogo extends StatelessWidget { @override Widget build(BuildContext context) { return MouseEventsRegion( - builder: (context, states) { - final isHovered = states.contains(MaterialState.hovered); - final colorBlendFactor = isHovered ? 0.5 : 0.6; + builder: (context, states) { + final isHovered = states.contains(WidgetState.hovered); + final colorBlendFactor = isHovered ? 0.5 : 0.6; - return Container( - foregroundDecoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withOpacity(colorBlendFactor), - backgroundBlendMode: BlendMode.color, - ), - child: Image.asset( - Assets.logoWide, - fit: BoxFit.scaleDown, - ), - ); - }, - onTap: onTap); + return Container( + foregroundDecoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primary + .withOpacity(colorBlendFactor), + backgroundBlendMode: BlendMode.color, + ), + child: Image.asset( + Assets.logoWide, + fit: BoxFit.scaleDown, + ), + ); + }, + onTap: onTap, + ); } } diff --git a/designer_v2/lib/common_views/styling_information.dart b/designer_v2/lib/common_views/styling_information.dart index e435fcd54..e7d563aac 100644 --- a/designer_v2/lib/common_views/styling_information.dart +++ b/designer_v2/lib/common_views/styling_information.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:studyu_designer_v2/common_views/banner.dart'; import 'package:studyu_designer_v2/common_views/text_hyperlink.dart'; import 'package:studyu_designer_v2/common_views/text_paragraph.dart'; import 'package:studyu_designer_v2/theme.dart'; -import 'banner.dart'; - class HtmlStylingBanner extends StatelessWidget { - const HtmlStylingBanner({this.isDismissed = false, this.onDismissed, super.key}); + const HtmlStylingBanner({ + this.isDismissed = false, + this.onDismissed, + super.key, + }); final bool isDismissed; final Function()? onDismissed; @@ -41,45 +44,60 @@ class HtmlStylingBanner extends StatelessWidget { }, defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ - TableRow(children: [ - const Text('Make your text bold'), - Text('Bold text', style: ThemeConfig.bodyTextMuted(theme)) - ]), - TableRow(children: [ - const Text('Add a hyperlink'), - Text( - 'A hyperlink', - style: ThemeConfig.bodyTextMuted(theme), - ) - ]), - TableRow(children: [ - const Text('Make your text colorful'), - Text( - 'Red text', - style: ThemeConfig.bodyTextMuted(theme), - ), - ]), - TableRow(children: [ - const Text('Add an image'), - Text( - '', - style: ThemeConfig.bodyTextMuted(theme), - ) - ]), - TableRow(children: [ - const Text('Add a video'), - Text( - '