diff --git a/.github/workflows/example-pull-request-preview.yml b/.github/workflows/example-pull-request-preview.yml index 7d4bb222..6aa9e05c 100644 --- a/.github/workflows/example-pull-request-preview.yml +++ b/.github/workflows/example-pull-request-preview.yml @@ -3,6 +3,7 @@ on: pull_request: paths: - "apps/example/**" + - "apps/example_liquid_glass/**" - "packages/**" jobs: example_preview: @@ -17,16 +18,43 @@ jobs: channel: 'stable' cache: true cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' - - name: Run Build + - name: Build Example working-directory: ./apps/example run: | flutter pub get flutter build web --release - - uses: FirebaseExtended/action-hosting-deploy@v0 + - name: Deploy Example Preview + uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: '${{ secrets.GITHUB_TOKEN }}' firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_AP_COMMON }}' projectId: ap-common entryPoint: "apps/example" channelId: "example-preview" + expires: 7d + liquid_glass_preview: + if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.38.x' + channel: 'stable' + cache: true + cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' + - name: Build Liquid Glass Example + working-directory: ./apps/example_liquid_glass + run: | + flutter pub get + flutter build web --release + - name: Deploy Liquid Glass Preview + uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: '${{ secrets.GITHUB_TOKEN }}' + firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_AP_COMMON }}' + projectId: ap-common + entryPoint: "apps/example_liquid_glass" + channelId: "liquid-glass-preview" expires: 7d \ No newline at end of file diff --git a/apps/example_liquid_glass/.gitignore b/apps/example_liquid_glass/.gitignore new file mode 100644 index 00000000..3820a95c --- /dev/null +++ b/apps/example_liquid_glass/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/apps/example_liquid_glass/.metadata b/apps/example_liquid_glass/.metadata new file mode 100644 index 00000000..84205a9c --- /dev/null +++ b/apps/example_liquid_glass/.metadata @@ -0,0 +1,42 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + base_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + - platform: android + create_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + base_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + - platform: ios + create_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + base_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + - platform: linux + create_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + base_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + - platform: macos + create_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + base_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + - platform: windows + create_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + base_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/apps/example_liquid_glass/analysis_options.yaml b/apps/example_liquid_glass/analysis_options.yaml new file mode 100644 index 00000000..a3008fc9 --- /dev/null +++ b/apps/example_liquid_glass/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lint/strict.yaml diff --git a/apps/example_liquid_glass/android/.gitignore b/apps/example_liquid_glass/android/.gitignore new file mode 100644 index 00000000..be3943c9 --- /dev/null +++ b/apps/example_liquid_glass/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/apps/example_liquid_glass/android/app/build.gradle.kts b/apps/example_liquid_glass/android/app/build.gradle.kts new file mode 100644 index 00000000..86f09bd5 --- /dev/null +++ b/apps/example_liquid_glass/android/app/build.gradle.kts @@ -0,0 +1,48 @@ +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") +} + +android { + namespace = "com.ap.common.liquidglass.ap_common_example_liquid_glass" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + // Flag to enable support for the new language APIs + isCoreLibraryDesugaringEnabled = true + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + defaultConfig { + applicationId = "com.ap.common.liquidglass.ap_common_example_liquid_glass" + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + multiDexEnabled = true + } + + buildTypes { + release { + signingConfig = signingConfigs.getByName("debug") + } + } +} + +dependencies { + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2") + implementation("androidx.window:window:1.0.0") + implementation("androidx.window:window-java:1.0.0") +} + +flutter { + source = "../.." +} diff --git a/apps/example_liquid_glass/android/app/src/debug/AndroidManifest.xml b/apps/example_liquid_glass/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/apps/example_liquid_glass/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/apps/example_liquid_glass/android/app/src/main/AndroidManifest.xml b/apps/example_liquid_glass/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1494afae --- /dev/null +++ b/apps/example_liquid_glass/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/example_liquid_glass/android/app/src/main/kotlin/com/ap/common/liquidglass/ap_common_example_liquid_glass/MainActivity.kt b/apps/example_liquid_glass/android/app/src/main/kotlin/com/ap/common/liquidglass/ap_common_example_liquid_glass/MainActivity.kt new file mode 100644 index 00000000..cbf01a61 --- /dev/null +++ b/apps/example_liquid_glass/android/app/src/main/kotlin/com/ap/common/liquidglass/ap_common_example_liquid_glass/MainActivity.kt @@ -0,0 +1,5 @@ +package com.ap.common.liquidglass.ap_common_example_liquid_glass + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/apps/example_liquid_glass/android/app/src/main/res/drawable-v21/launch_background.xml b/apps/example_liquid_glass/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/apps/example_liquid_glass/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/apps/example_liquid_glass/android/app/src/main/res/drawable/launch_background.xml b/apps/example_liquid_glass/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/apps/example_liquid_glass/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/apps/example_liquid_glass/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/apps/example_liquid_glass/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..db77bb4b Binary files /dev/null and b/apps/example_liquid_glass/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/apps/example_liquid_glass/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/apps/example_liquid_glass/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..17987b79 Binary files /dev/null and b/apps/example_liquid_glass/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/apps/example_liquid_glass/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/apps/example_liquid_glass/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..09d43914 Binary files /dev/null and b/apps/example_liquid_glass/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/apps/example_liquid_glass/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/apps/example_liquid_glass/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..d5f1c8d3 Binary files /dev/null and b/apps/example_liquid_glass/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/apps/example_liquid_glass/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/apps/example_liquid_glass/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..4d6372ee Binary files /dev/null and b/apps/example_liquid_glass/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/apps/example_liquid_glass/android/app/src/main/res/values-night/styles.xml b/apps/example_liquid_glass/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/apps/example_liquid_glass/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/apps/example_liquid_glass/android/app/src/main/res/values/styles.xml b/apps/example_liquid_glass/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..322503e4 --- /dev/null +++ b/apps/example_liquid_glass/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/apps/example_liquid_glass/android/app/src/profile/AndroidManifest.xml b/apps/example_liquid_glass/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/apps/example_liquid_glass/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/apps/example_liquid_glass/android/build.gradle.kts b/apps/example_liquid_glass/android/build.gradle.kts new file mode 100644 index 00000000..dbee657b --- /dev/null +++ b/apps/example_liquid_glass/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/apps/example_liquid_glass/android/gradle.properties b/apps/example_liquid_glass/android/gradle.properties new file mode 100644 index 00000000..25971708 --- /dev/null +++ b/apps/example_liquid_glass/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/apps/example_liquid_glass/android/gradle/wrapper/gradle-wrapper.properties b/apps/example_liquid_glass/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..efdcc4ac --- /dev/null +++ b/apps/example_liquid_glass/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip diff --git a/apps/example_liquid_glass/android/settings.gradle.kts b/apps/example_liquid_glass/android/settings.gradle.kts new file mode 100644 index 00000000..ec259148 --- /dev/null +++ b/apps/example_liquid_glass/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.9.1" apply false + id("org.jetbrains.kotlin.android") version "1.9.10" apply false +} + +include(":app") diff --git a/apps/example_liquid_glass/assets b/apps/example_liquid_glass/assets new file mode 120000 index 00000000..6f2de55f --- /dev/null +++ b/apps/example_liquid_glass/assets @@ -0,0 +1 @@ +../example/assets \ No newline at end of file diff --git a/apps/example_liquid_glass/firebase.json b/apps/example_liquid_glass/firebase.json new file mode 100644 index 00000000..0d25a779 --- /dev/null +++ b/apps/example_liquid_glass/firebase.json @@ -0,0 +1,10 @@ +{ + "hosting": { + "public": "build/web", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ] + } +} diff --git a/apps/example_liquid_glass/ios/.gitignore b/apps/example_liquid_glass/ios/.gitignore new file mode 100644 index 00000000..7a7f9873 --- /dev/null +++ b/apps/example_liquid_glass/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/apps/example_liquid_glass/ios/Flutter/AppFrameworkInfo.plist b/apps/example_liquid_glass/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..1dc6cf76 --- /dev/null +++ b/apps/example_liquid_glass/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/apps/example_liquid_glass/ios/Flutter/Debug.xcconfig b/apps/example_liquid_glass/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..ec97fc6f --- /dev/null +++ b/apps/example_liquid_glass/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/apps/example_liquid_glass/ios/Flutter/Release.xcconfig b/apps/example_liquid_glass/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..c4855bfe --- /dev/null +++ b/apps/example_liquid_glass/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/apps/example_liquid_glass/ios/Runner.xcodeproj/project.pbxproj b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..fd726013 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,748 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 7104B2AF98D7133B5FC8DE3B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D4579526FB03E66736E237A1 /* Pods_Runner.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + FECA9E8B24DD661D8FA73851 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DF35E34B094B13C437FDC031 /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AE35F34F5A1F6FA0BD6A6F03 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + B0E23A5F722B353FB3A31582 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + C7D4A2DE5F9353A9DE721BB9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + D4579526FB03E66736E237A1 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D95C3C6CB7B334AA2FCF8D36 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + DF35E34B094B13C437FDC031 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DF7A69D88620A4E3E4C6860C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + E5123AB7DD5F6D3BB99CF4DD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7104B2AF98D7133B5FC8DE3B /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2B3BB2D39C038EF76746AF6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FECA9E8B24DD661D8FA73851 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 5287E88233F1450AB8717CE9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D4579526FB03E66736E237A1 /* Pods_Runner.framework */, + DF35E34B094B13C437FDC031 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 7F691E62A5A6E0D67909DD01 /* Pods */ = { + isa = PBXGroup; + children = ( + C7D4A2DE5F9353A9DE721BB9 /* Pods-Runner.debug.xcconfig */, + E5123AB7DD5F6D3BB99CF4DD /* Pods-Runner.release.xcconfig */, + D95C3C6CB7B334AA2FCF8D36 /* Pods-Runner.profile.xcconfig */, + DF7A69D88620A4E3E4C6860C /* Pods-RunnerTests.debug.xcconfig */, + AE35F34F5A1F6FA0BD6A6F03 /* Pods-RunnerTests.release.xcconfig */, + B0E23A5F722B353FB3A31582 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 7F691E62A5A6E0D67909DD01 /* Pods */, + 5287E88233F1450AB8717CE9 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 7726C7D82DFB077AD6384DC6 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + D2B3BB2D39C038EF76746AF6 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 1F3508AD5B113DC27785DE47 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 4EED1E5ED4FD5878D315FC9A /* [CP] Embed Pods Frameworks */, + 9CA632B520412222511A6633 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1F3508AD5B113DC27785DE47 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 4EED1E5ED4FD5878D315FC9A /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 7726C7D82DFB077AD6384DC6 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + 9CA632B520412222511A6633 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 5SP52F6F2C; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DF7A69D88620A4E3E4C6860C /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AE35F34F5A1F6FA0BD6A6F03 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B0E23A5F722B353FB3A31582 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 5SP52F6F2C; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 5SP52F6F2C; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/apps/example_liquid_glass/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/apps/example_liquid_glass/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..e3773d42 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/example_liquid_glass/ios/Runner.xcworkspace/contents.xcworkspacedata b/apps/example_liquid_glass/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/apps/example_liquid_glass/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/example_liquid_glass/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/example_liquid_glass/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/example_liquid_glass/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/apps/example_liquid_glass/ios/Runner/AppDelegate.swift b/apps/example_liquid_glass/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..c30b367e --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..dc9ada47 Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..7353c41e Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..6ed2d933 Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..4cd7b009 Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..fe730945 Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..321773cd Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..502f463a Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..e9f5fea2 Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..84ac32ae Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..8953cba0 Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..0467bf12 Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/apps/example_liquid_glass/ios/Runner/Base.lproj/LaunchScreen.storyboard b/apps/example_liquid_glass/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/example_liquid_glass/ios/Runner/Base.lproj/Main.storyboard b/apps/example_liquid_glass/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/example_liquid_glass/ios/Runner/Info.plist b/apps/example_liquid_glass/ios/Runner/Info.plist new file mode 100644 index 00000000..a6d7c160 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner/Info.plist @@ -0,0 +1,70 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Ap Common Example Liquid Glass + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ap_common_example_liquid_glass + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/apps/example_liquid_glass/ios/Runner/Runner-Bridging-Header.h b/apps/example_liquid_glass/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/apps/example_liquid_glass/ios/RunnerTests/RunnerTests.swift b/apps/example_liquid_glass/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..86a7c3b1 --- /dev/null +++ b/apps/example_liquid_glass/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/apps/example_liquid_glass/lib/app.dart b/apps/example_liquid_glass/lib/app.dart new file mode 100644 index 00000000..965c3031 --- /dev/null +++ b/apps/example_liquid_glass/lib/app.dart @@ -0,0 +1,40 @@ +import 'package:ap_common_example_liquid_glass/pages/home_page.dart'; +import 'package:ap_common_example_liquid_glass/pages/login_page.dart'; +import 'package:ap_common_liquid_glass/ap_common_liquid_glass.dart'; +import 'package:flutter/material.dart'; + +class LiquidGlassExampleApp extends StatelessWidget { + const LiquidGlassExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return LiquidGlassApApp( + onGenerateTitle: (_) => 'Liquid Glass Example', + home: const _RootPage(), + ); + } +} + +class _RootPage extends StatefulWidget { + const _RootPage(); + + @override + State<_RootPage> createState() => _RootPageState(); +} + +class _RootPageState extends State<_RootPage> { + bool _isLogin = false; + + @override + Widget build(BuildContext context) { + if (_isLogin) { + return GlassMainPage( + onLogout: () => setState(() => _isLogin = false), + ); + } + return GlassLoginPage( + onLoginSuccess: () => + setState(() => _isLogin = true), + ); + } +} diff --git a/apps/example_liquid_glass/lib/main.dart b/apps/example_liquid_glass/lib/main.dart new file mode 100644 index 00000000..4666e64e --- /dev/null +++ b/apps/example_liquid_glass/lib/main.dart @@ -0,0 +1,20 @@ +import 'package:ap_common/ap_common.dart' + hide AppLocaleUtils, TranslationProvider; +import 'package:ap_common_example_liquid_glass/app.dart'; +import 'package:ap_common_liquid_glass/ap_common_liquid_glass.dart'; +import 'package:encrypt/encrypt.dart' as encrypt; +import 'package:flutter/material.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await LiquidGlassWidgets.initialize(); + registerOneForAll(); + registerApCommonService(); + await (PreferenceUtil.instance as ApPreferenceUtil).init( + key: encrypt.Key.fromUtf8('l9r1W3wcsnJTayxCXwoFt62w1i4sQ5J9'), + iv: encrypt.IV.fromUtf8('auc9OV5r0nLwjCAH'), + ); + runApp( + LiquidGlassWidgets.wrap(const LiquidGlassExampleApp()), + ); +} diff --git a/apps/example_liquid_glass/lib/pages/course_page.dart b/apps/example_liquid_glass/lib/pages/course_page.dart new file mode 100644 index 00000000..aefe9396 --- /dev/null +++ b/apps/example_liquid_glass/lib/pages/course_page.dart @@ -0,0 +1,26 @@ +import 'package:ap_common_example_liquid_glass/res/assets.dart'; +import 'package:ap_common_liquid_glass/ap_common_liquid_glass.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Course page using [GlassApCoursePage]. +class GlassCoursePage extends StatelessWidget { + const GlassCoursePage({super.key}); + + @override + Widget build(BuildContext context) { + return GlassApCoursePage( + onLoadSemesters: () async { + final String rawString = + await rootBundle.loadString(FileAssets.semesters); + return SemesterData.fromRawJson(rawString); + }, + onLoadCourse: (Semester semester) async { + final String rawString = + await rootBundle.loadString(FileAssets.courses); + return CourseData.fromRawJson(rawString); + }, + enableCaptureCourseTable: true, + ); + } +} diff --git a/apps/example_liquid_glass/lib/pages/home_page.dart b/apps/example_liquid_glass/lib/pages/home_page.dart new file mode 100644 index 00000000..357dcf4e --- /dev/null +++ b/apps/example_liquid_glass/lib/pages/home_page.dart @@ -0,0 +1,341 @@ +import 'package:ap_common_announcement_ui/ap_common_announcement_ui.dart'; +import 'package:ap_common_example_liquid_glass/pages/course_page.dart'; +import 'package:ap_common_example_liquid_glass/pages/score_page.dart'; +import 'package:ap_common_example_liquid_glass/res/assets.dart'; +import 'package:ap_common_liquid_glass/ap_common_liquid_glass.dart'; +import 'package:flutter/material.dart'; + +/// Main page after login with bottom tab navigation. +class GlassMainPage extends StatefulWidget { + const GlassMainPage({ + super.key, + required this.onLogout, + }); + + final VoidCallback onLogout; + + @override + State createState() => + _GlassMainPageState(); +} + +class _GlassMainPageState extends State { + int _selectedIndex = 0; + + HomeState _homeState = HomeState.loading; + List _announcements = []; + + @override + void initState() { + super.initState(); + _getAnnouncements(); + } + + Future _getAnnouncements() async { + final ApiResult> result = + await AnnouncementHelper.instance + .getAnnouncements(tags: []); + if (!mounted) return; + switch (result) { + case ApiSuccess>( + :final List data, + ): + setState(() { + _announcements = data; + _homeState = data.isEmpty + ? HomeState.empty + : HomeState.finish; + }); + case ApiFailure>(): + case ApiError>(): + setState(() => _homeState = HomeState.error); + } + } + + @override + Widget build(BuildContext context) { + final ApLocalizations ap = context.ap; + + return AdaptiveLiquidGlassLayer( + child: Scaffold( + drawer: _buildDrawer(context, ap), + body: Stack( + children: [ + IndexedStack( + index: _selectedIndex, + children: [ + _buildHomeTab(), + const GlassCoursePage(), + const GlassScorePage(), + _buildSettingsTab(), + ], + ), + Positioned( + left: 16, + right: MediaQuery.of(context) + .size + .width * + 0.2, + bottom: + MediaQuery.of(context) + .padding + .bottom + + 8, + child: Builder( + builder: (BuildContext ctx) { + final bool isDark = + Theme.of(ctx).brightness == + Brightness.dark; + final Color iconColor = isDark + ? Colors.white + : Colors.black; + return GlassBottomBar( + tabs: [ + GlassBottomBarTab( + icon: Icon(ApIcon.home), + label: ap.home, + ), + GlassBottomBarTab( + icon: Icon(ApIcon.classIcon), + label: ap.course, + ), + GlassBottomBarTab( + icon: Icon(ApIcon.assignment), + label: ap.score, + ), + GlassBottomBarTab( + icon: Icon(ApIcon.person), + label: ap.settings, + ), + ], + selectedIndex: _selectedIndex, + onTabSelected: (int index) { + setState( + () => + _selectedIndex = index, + ); + }, + barHeight: 52, + barBorderRadius: 26, + horizontalPadding: 10, + verticalPadding: 10, + iconSize: 22, + selectedIconColor: iconColor, + unselectedIconColor: + iconColor.withAlpha(153), + textStyle: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: iconColor, + ), + ); + }, + ), + ), + ], + ), + ), + ); + } + + Widget _buildHomeTab() { + return GlassHomePageScaffold( + state: _homeState, + announcements: _announcements, + isLogin: true, + title: 'Liquid Glass', + bottomPadding: 80, + actions: [ + IconButton( + icon: const Icon(Icons.brightness_6), + onPressed: () { + final LiquidGlassApAppState state = + LiquidGlassApApp.of(context); + state.setThemeMode( + state.themeMode == ThemeMode.dark + ? ThemeMode.light + : ThemeMode.dark, + ); + }, + iconSize: 22, + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 36, + minHeight: 36, + ), + ), + ], + onImageTapped: (Announcement announcement) { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + GlassAnnouncementContentPage( + announcement: announcement, + ), + ), + ); + }, + ); + } + + Widget _buildDrawer( + BuildContext context, + ApLocalizations ap, + ) { + return GlassApDrawer( + imageAsset: ImageAssets.K, + userInfo: const UserInfo( + id: 'C000000000', + name: 'AP Example', + className: '資工四甲', + department: '資訊工程系', + ), + onTapHeader: () {}, + widgets: [ + DrawerItem( + icon: ApIcon.home, + title: ap.home, + onTap: () { + Navigator.pop(context); + setState(() => _selectedIndex = 0); + }, + ), + DrawerItem( + icon: ApIcon.classIcon, + title: ap.course, + onTap: () { + Navigator.pop(context); + setState(() => _selectedIndex = 1); + }, + ), + DrawerItem( + icon: ApIcon.assignment, + title: ap.score, + onTap: () { + Navigator.pop(context); + setState(() => _selectedIndex = 2); + }, + ), + DrawerItem( + icon: ApIcon.settings, + title: ap.settings, + onTap: () { + Navigator.pop(context); + setState(() => _selectedIndex = 3); + }, + ), + const GlassDivider(), + DrawerItem( + icon: ApIcon.powerSettingsNew, + title: ap.logout, + onTap: () { + Navigator.pop(context); + widget.onLogout(); + }, + ), + ], + ); + } + + Widget _buildSettingsTab() { + final ApLocalizations ap = context.ap; + final LiquidGlassApAppState appState = + LiquidGlassApApp.of(context); + + return AdaptiveLiquidGlassLayer( + child: Scaffold( + body: Stack( + children: [ + ListView( + padding: EdgeInsets.only( + top: + MediaQuery.of(context).padding.top + + 60 + + 16, + left: 16, + right: 16, + bottom: 80, + ), + children: [ + GlassCard( + child: Column( + children: [ + GlassListTile( + leading: const Icon( + Icons.brightness_6, + ), + title: const Text('Dark Mode'), + trailing: GlassSwitch( + value: appState.themeMode == + ThemeMode.dark, + onChanged: (bool value) { + appState.setThemeMode( + value + ? ThemeMode.dark + : ThemeMode.light, + ); + }, + ), + ), + const GlassDivider(), + GlassListTile( + leading: const Icon( + Icons.info_outline, + ), + title: Text(ap.about), + trailing: GlassListTile.chevron, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + const GlassAboutUsPage( + assetImage: + ImageAssets.K, + githubName: 'NKUST-ITC', + email: + 'abc873693@gmail.com', + appLicense: + 'MIT License', + fbFanPageId: + '735951703168873', + fbFanPageUrl: + 'https://www.facebook.com/NKUST.ITC/', + githubUrl: + 'https://github.com/NKUST-ITC', + ), + ), + ); + }, + ), + const GlassDivider(), + GlassListTile( + leading: const Icon( + Icons.logout, + ), + title: Text(ap.logout), + onTap: widget.onLogout, + ), + ], + ), + ), + ], + ), + GlassFloatingToolbar( + leading: [ + Text( + ap.settings, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/apps/example_liquid_glass/lib/pages/login_page.dart b/apps/example_liquid_glass/lib/pages/login_page.dart new file mode 100644 index 00000000..d560b45f --- /dev/null +++ b/apps/example_liquid_glass/lib/pages/login_page.dart @@ -0,0 +1,203 @@ +import 'package:ap_common_example_liquid_glass/res/assets.dart'; +import 'package:ap_common_liquid_glass/ap_common_liquid_glass.dart'; +import 'package:flutter/material.dart'; + +/// Login page using [GlassLoginScaffold] with glass widgets. +class GlassLoginPage extends StatefulWidget { + const GlassLoginPage({ + super.key, + required this.onLoginSuccess, + }); + + final VoidCallback onLoginSuccess; + + @override + State createState() => + _GlassLoginPageState(); +} + +class _GlassLoginPageState extends State { + final TextEditingController _username = + TextEditingController(); + final TextEditingController _password = + TextEditingController(); + final FocusNode _usernameFocus = FocusNode(); + final FocusNode _passwordFocus = FocusNode(); + + @override + void dispose() { + _username.dispose(); + _password.dispose(); + _usernameFocus.dispose(); + _passwordFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final ApLocalizations ap = context.ap; + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return GlassLoginScaffold( + logoMode: LogoMode.image, + logoSource: ImageAssets.K, + forms: [ + TextField( + controller: _username, + focusNode: _usernameFocus, + textInputAction: TextInputAction.next, + onSubmitted: (_) => + _passwordFocus.requestFocus(), + style: TextStyle( + color: colorScheme.onSurface, + ), + decoration: InputDecoration( + hintText: ap.studentId, + hintStyle: TextStyle( + color: colorScheme.onSurface + .withAlpha(128), + ), + filled: false, + border: OutlineInputBorder( + borderRadius: + BorderRadius.circular(10), + borderSide: BorderSide( + color: colorScheme.onSurface + .withAlpha(77), + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: + BorderRadius.circular(10), + borderSide: BorderSide( + color: colorScheme.onSurface + .withAlpha(77), + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: + BorderRadius.circular(10), + borderSide: BorderSide( + color: colorScheme.primary, + ), + ), + contentPadding: + const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + const SizedBox(height: 12), + TextField( + controller: _password, + focusNode: _passwordFocus, + obscureText: true, + textInputAction: TextInputAction.send, + onSubmitted: (_) => _login(), + style: TextStyle( + color: colorScheme.onSurface, + ), + decoration: InputDecoration( + hintText: ap.password, + hintStyle: TextStyle( + color: colorScheme.onSurface + .withAlpha(128), + ), + filled: false, + border: OutlineInputBorder( + borderRadius: + BorderRadius.circular(10), + borderSide: BorderSide( + color: colorScheme.onSurface + .withAlpha(77), + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: + BorderRadius.circular(10), + borderSide: BorderSide( + color: colorScheme.onSurface + .withAlpha(77), + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: + BorderRadius.circular(10), + borderSide: BorderSide( + color: colorScheme.primary, + ), + ), + contentPadding: + const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + const SizedBox(height: 20), + GlassButton.custom( + onTap: _login, + width: double.infinity, + height: 48, + shape: const LiquidRoundedSuperellipse( + borderRadius: 12, + ), + child: Text( + ap.login, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + ), + ], + ); + } + + Future _login() async { + final ApLocalizations ap = context.ap; + + if (_username.text.isEmpty || + _password.text.isEmpty) { + UiUtil.instance.showToast( + context, + ap.doNotEmpty, + ); + return; + } + + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => PopScope( + canPop: false, + child: Center( + child: GlassCard( + useOwnLayer: true, + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const GlassProgressIndicator + .circular(), + const SizedBox(height: 16), + Text(ap.logining), + ], + ), + ), + ), + ), + ); + + await Future.delayed( + const Duration(seconds: 1), + ); + + if (!mounted) return; + Navigator.of(context, rootNavigator: true).pop(); + widget.onLoginSuccess(); + } +} diff --git a/apps/example_liquid_glass/lib/pages/score_page.dart b/apps/example_liquid_glass/lib/pages/score_page.dart new file mode 100644 index 00000000..bbd52a69 --- /dev/null +++ b/apps/example_liquid_glass/lib/pages/score_page.dart @@ -0,0 +1,33 @@ +import 'package:ap_common_example_liquid_glass/res/assets.dart'; +import 'package:ap_common_liquid_glass/ap_common_liquid_glass.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Score page using [GlassApScorePage]. +class GlassScorePage extends StatelessWidget { + const GlassScorePage({super.key}); + + @override + Widget build(BuildContext context) { + final ApLocalizations ap = context.ap; + + return GlassApScorePage( + onLoadSemesters: () async { + final String rawString = + await rootBundle.loadString(FileAssets.semesters); + return SemesterData.fromRawJson(rawString); + }, + onLoadScore: (Semester semester) async { + final String rawString = + await rootBundle.loadString(FileAssets.scores); + return ScoreData.fromRawJson(rawString); + }, + detailBuilder: (ScoreData? scoreData) => [ + '${ap.conductScore}:${scoreData?.detail.conduct ?? ''}', + '${ap.average}:${scoreData?.detail.average ?? ''}', + '${ap.classRank}:${scoreData?.detail.classRank ?? ''}', + '${ap.departmentRank}:${scoreData?.detail.departmentRank ?? ''}', + ], + ); + } +} diff --git a/apps/example_liquid_glass/lib/res/assets.dart b/apps/example_liquid_glass/lib/res/assets.dart new file mode 100644 index 00000000..21d7a083 --- /dev/null +++ b/apps/example_liquid_glass/lib/res/assets.dart @@ -0,0 +1,13 @@ +class ImageAssets { + static const String basePath = 'assets/images'; + + static const String K = '$basePath/K.webp'; +} + +class FileAssets { + static const String basePath = 'assets'; + + static const String courses = '$basePath/coursetable.json'; + static const String scores = '$basePath/scores.json'; + static const String semesters = '$basePath/semesters.json'; +} diff --git a/apps/example_liquid_glass/linux/.gitignore b/apps/example_liquid_glass/linux/.gitignore new file mode 100644 index 00000000..d3896c98 --- /dev/null +++ b/apps/example_liquid_glass/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/apps/example_liquid_glass/linux/CMakeLists.txt b/apps/example_liquid_glass/linux/CMakeLists.txt new file mode 100644 index 00000000..2076e14d --- /dev/null +++ b/apps/example_liquid_glass/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "ap_common_example_liquid_glass") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.ap.common.liquidglass.ap_common_example_liquid_glass") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/apps/example_liquid_glass/linux/flutter/CMakeLists.txt b/apps/example_liquid_glass/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..d5bd0164 --- /dev/null +++ b/apps/example_liquid_glass/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/apps/example_liquid_glass/linux/flutter/generated_plugin_registrant.cc b/apps/example_liquid_glass/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..1ef38c29 --- /dev/null +++ b/apps/example_liquid_glass/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,27 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_saver_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin"); + file_saver_plugin_register_with_registrar(file_saver_registrar); + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) printing_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin"); + printing_plugin_register_with_registrar(printing_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/apps/example_liquid_glass/linux/flutter/generated_plugin_registrant.h b/apps/example_liquid_glass/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/apps/example_liquid_glass/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/apps/example_liquid_glass/linux/flutter/generated_plugins.cmake b/apps/example_liquid_glass/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..cba264c8 --- /dev/null +++ b/apps/example_liquid_glass/linux/flutter/generated_plugins.cmake @@ -0,0 +1,27 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_saver + file_selector_linux + printing + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/apps/example_liquid_glass/linux/runner/CMakeLists.txt b/apps/example_liquid_glass/linux/runner/CMakeLists.txt new file mode 100644 index 00000000..e97dabc7 --- /dev/null +++ b/apps/example_liquid_glass/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/apps/example_liquid_glass/linux/runner/main.cc b/apps/example_liquid_glass/linux/runner/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/apps/example_liquid_glass/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/apps/example_liquid_glass/linux/runner/my_application.cc b/apps/example_liquid_glass/linux/runner/my_application.cc new file mode 100644 index 00000000..e947e4ad --- /dev/null +++ b/apps/example_liquid_glass/linux/runner/my_application.cc @@ -0,0 +1,148 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "ap_common_example_liquid_glass"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "ap_common_example_liquid_glass"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/apps/example_liquid_glass/linux/runner/my_application.h b/apps/example_liquid_glass/linux/runner/my_application.h new file mode 100644 index 00000000..db16367a --- /dev/null +++ b/apps/example_liquid_glass/linux/runner/my_application.h @@ -0,0 +1,21 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, + my_application, + MY, + APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/apps/example_liquid_glass/macos/.gitignore b/apps/example_liquid_glass/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /dev/null +++ b/apps/example_liquid_glass/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/apps/example_liquid_glass/macos/Flutter/Flutter-Debug.xcconfig b/apps/example_liquid_glass/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/apps/example_liquid_glass/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/apps/example_liquid_glass/macos/Flutter/Flutter-Release.xcconfig b/apps/example_liquid_glass/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/apps/example_liquid_glass/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/apps/example_liquid_glass/macos/Runner.xcodeproj/project.pbxproj b/apps/example_liquid_glass/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..90929011 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,840 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 877BE9C6444A2222CA96448D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9ED21E24E2592E459A0048DD /* Pods_RunnerTests.framework */; }; + 921885A9670B88D62FD64AF3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7323D157E85AA2DEF564E19F /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* ap_common_example_liquid_glass.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ap_common_example_liquid_glass.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7323D157E85AA2DEF564E19F /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 947A67C3FDFE60B13F42508E /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 9D26850C2DE263EBF4F94647 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 9ED21E24E2592E459A0048DD /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9F96348047BCB77E822E7D59 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + BE26E5BBE785D3E9D616D90C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + DB1ACD1AC0DB22A77BFD2CE9 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + FCB57B8043F6B7849761FE7D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 877BE9C6444A2222CA96448D /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 921885A9670B88D62FD64AF3 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + BFC6AD02D4EB304802EC6B7B /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* ap_common_example_liquid_glass.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + BFC6AD02D4EB304802EC6B7B /* Pods */ = { + isa = PBXGroup; + children = ( + BE26E5BBE785D3E9D616D90C /* Pods-Runner.debug.xcconfig */, + 9F96348047BCB77E822E7D59 /* Pods-Runner.release.xcconfig */, + FCB57B8043F6B7849761FE7D /* Pods-Runner.profile.xcconfig */, + DB1ACD1AC0DB22A77BFD2CE9 /* Pods-RunnerTests.debug.xcconfig */, + 9D26850C2DE263EBF4F94647 /* Pods-RunnerTests.release.xcconfig */, + 947A67C3FDFE60B13F42508E /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7323D157E85AA2DEF564E19F /* Pods_Runner.framework */, + 9ED21E24E2592E459A0048DD /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + E6D94A79AC19F8A4E0414E0B /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 424ECA0403487CD85158166E /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 80D578BC7659B17A8D513EA2 /* [CP] Embed Pods Frameworks */, + B6D578792651946F4CB0D551 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* ap_common_example_liquid_glass.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 424ECA0403487CD85158166E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 80D578BC7659B17A8D513EA2 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + B6D578792651946F4CB0D551 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + E6D94A79AC19F8A4E0414E0B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DB1ACD1AC0DB22A77BFD2CE9 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ap_common_example_liquid_glass.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ap_common_example_liquid_glass"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9D26850C2DE263EBF4F94647 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ap_common_example_liquid_glass.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ap_common_example_liquid_glass"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 947A67C3FDFE60B13F42508E /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ap_common_example_liquid_glass.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ap_common_example_liquid_glass"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + ENABLE_APP_SANDBOX = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + ENABLE_APP_SANDBOX = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/apps/example_liquid_glass/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/example_liquid_glass/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/example_liquid_glass/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/apps/example_liquid_glass/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..edb543cb --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/example_liquid_glass/macos/Runner.xcworkspace/contents.xcworkspacedata b/apps/example_liquid_glass/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/apps/example_liquid_glass/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/example_liquid_glass/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/example_liquid_glass/macos/Runner/AppDelegate.swift b/apps/example_liquid_glass/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..b3c17614 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 00000000..82b6f9d9 Binary files /dev/null and b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 00000000..13b35eba Binary files /dev/null and b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 00000000..0a3f5fa4 Binary files /dev/null and b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 00000000..bdb57226 Binary files /dev/null and b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 00000000..f083318e Binary files /dev/null and b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 00000000..326c0e72 Binary files /dev/null and b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 00000000..2f1632cf Binary files /dev/null and b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/apps/example_liquid_glass/macos/Runner/Base.lproj/MainMenu.xib b/apps/example_liquid_glass/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..80e867a4 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/example_liquid_glass/macos/Runner/Configs/AppInfo.xcconfig b/apps/example_liquid_glass/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..b9965e6a --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = ap_common_example_liquid_glass + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.ap.common.liquidglass. All rights reserved. diff --git a/apps/example_liquid_glass/macos/Runner/Configs/Debug.xcconfig b/apps/example_liquid_glass/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/apps/example_liquid_glass/macos/Runner/Configs/Release.xcconfig b/apps/example_liquid_glass/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/apps/example_liquid_glass/macos/Runner/Configs/Warnings.xcconfig b/apps/example_liquid_glass/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/apps/example_liquid_glass/macos/Runner/DebugProfile.entitlements b/apps/example_liquid_glass/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..dddb8a30 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/apps/example_liquid_glass/macos/Runner/Info.plist b/apps/example_liquid_glass/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/apps/example_liquid_glass/macos/Runner/MainFlutterWindow.swift b/apps/example_liquid_glass/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..3cc05eb2 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/apps/example_liquid_glass/macos/Runner/Release.entitlements b/apps/example_liquid_glass/macos/Runner/Release.entitlements new file mode 100644 index 00000000..852fa1a4 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/apps/example_liquid_glass/macos/RunnerTests/RunnerTests.swift b/apps/example_liquid_glass/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..61f3bd1f --- /dev/null +++ b/apps/example_liquid_glass/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/apps/example_liquid_glass/pubspec.yaml b/apps/example_liquid_glass/pubspec.yaml new file mode 100644 index 00000000..d0d625df --- /dev/null +++ b/apps/example_liquid_glass/pubspec.yaml @@ -0,0 +1,37 @@ +name: ap_common_example_liquid_glass +description: Liquid Glass example for ap_common. + +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: ^3.9.0 + +resolution: workspace + +dependencies: + ap_common: + path: ../../packages/ap_common + ap_common_announcement_ui: + path: ../../packages/ap_common_announcement_ui + ap_common_liquid_glass: + path: ../../packages/ap_common_liquid_glass + encrypt: any + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + slang: ^4.14.0 + slang_flutter: ^4.14.0 + +dev_dependencies: + flutter_test: + sdk: flutter + lint: ^2.1.2 + +flutter: + uses-material-design: true + assets: + - assets/images/ + - assets/ diff --git a/apps/example_liquid_glass/web/favicon.ico b/apps/example_liquid_glass/web/favicon.ico new file mode 100644 index 00000000..58f3621a Binary files /dev/null and b/apps/example_liquid_glass/web/favicon.ico differ diff --git a/apps/example_liquid_glass/web/icons/Icon-192.png b/apps/example_liquid_glass/web/icons/Icon-192.png new file mode 100644 index 00000000..f3599c6a Binary files /dev/null and b/apps/example_liquid_glass/web/icons/Icon-192.png differ diff --git a/apps/example_liquid_glass/web/icons/Icon-512.png b/apps/example_liquid_glass/web/icons/Icon-512.png new file mode 100644 index 00000000..c7f1ff85 Binary files /dev/null and b/apps/example_liquid_glass/web/icons/Icon-512.png differ diff --git a/apps/example_liquid_glass/web/icons/Icon-maskable-192.png b/apps/example_liquid_glass/web/icons/Icon-maskable-192.png new file mode 100644 index 00000000..f3599c6a Binary files /dev/null and b/apps/example_liquid_glass/web/icons/Icon-maskable-192.png differ diff --git a/apps/example_liquid_glass/web/icons/Icon-maskable-512.png b/apps/example_liquid_glass/web/icons/Icon-maskable-512.png new file mode 100644 index 00000000..c7f1ff85 Binary files /dev/null and b/apps/example_liquid_glass/web/icons/Icon-maskable-512.png differ diff --git a/apps/example_liquid_glass/web/index.html b/apps/example_liquid_glass/web/index.html new file mode 100644 index 00000000..d074ceb7 --- /dev/null +++ b/apps/example_liquid_glass/web/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + 校務通 Liquid Glass + + + + + + + + + + \ No newline at end of file diff --git a/apps/example_liquid_glass/web/manifest.json b/apps/example_liquid_glass/web/manifest.json new file mode 100644 index 00000000..65b120f2 --- /dev/null +++ b/apps/example_liquid_glass/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "nkust_ap", + "short_name": "nkust_ap", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "史上最強高科大校務系統 APP", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/apps/example_liquid_glass/windows/.gitignore b/apps/example_liquid_glass/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/apps/example_liquid_glass/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/apps/example_liquid_glass/windows/CMakeLists.txt b/apps/example_liquid_glass/windows/CMakeLists.txt new file mode 100644 index 00000000..061c9312 --- /dev/null +++ b/apps/example_liquid_glass/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(ap_common_example_liquid_glass LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "ap_common_example_liquid_glass") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/apps/example_liquid_glass/windows/flutter/CMakeLists.txt b/apps/example_liquid_glass/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..903f4899 --- /dev/null +++ b/apps/example_liquid_glass/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/apps/example_liquid_glass/windows/flutter/generated_plugin_registrant.cc b/apps/example_liquid_glass/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..cd55fe34 --- /dev/null +++ b/apps/example_liquid_glass/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSaverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSaverPlugin")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + PrintingPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PrintingPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/apps/example_liquid_glass/windows/flutter/generated_plugin_registrant.h b/apps/example_liquid_glass/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/apps/example_liquid_glass/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/apps/example_liquid_glass/windows/flutter/generated_plugins.cmake b/apps/example_liquid_glass/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..802c4136 --- /dev/null +++ b/apps/example_liquid_glass/windows/flutter/generated_plugins.cmake @@ -0,0 +1,28 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_saver + file_selector_windows + printing + share_plus + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/apps/example_liquid_glass/windows/runner/CMakeLists.txt b/apps/example_liquid_glass/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..394917c0 --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/apps/example_liquid_glass/windows/runner/Runner.rc b/apps/example_liquid_glass/windows/runner/Runner.rc new file mode 100644 index 00000000..5c4d33ba --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.ap.common.liquidglass" "\0" + VALUE "FileDescription", "ap_common_example_liquid_glass" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "ap_common_example_liquid_glass" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 com.ap.common.liquidglass. All rights reserved." "\0" + VALUE "OriginalFilename", "ap_common_example_liquid_glass.exe" "\0" + VALUE "ProductName", "ap_common_example_liquid_glass" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/apps/example_liquid_glass/windows/runner/flutter_window.cpp b/apps/example_liquid_glass/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..955ee303 --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/apps/example_liquid_glass/windows/runner/flutter_window.h b/apps/example_liquid_glass/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/apps/example_liquid_glass/windows/runner/main.cpp b/apps/example_liquid_glass/windows/runner/main.cpp new file mode 100644 index 00000000..58273a5e --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"ap_common_example_liquid_glass", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/apps/example_liquid_glass/windows/runner/resource.h b/apps/example_liquid_glass/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/apps/example_liquid_glass/windows/runner/resources/app_icon.ico b/apps/example_liquid_glass/windows/runner/resources/app_icon.ico new file mode 100644 index 00000000..c04e20ca Binary files /dev/null and b/apps/example_liquid_glass/windows/runner/resources/app_icon.ico differ diff --git a/apps/example_liquid_glass/windows/runner/runner.exe.manifest b/apps/example_liquid_glass/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..153653e8 --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/apps/example_liquid_glass/windows/runner/utils.cpp b/apps/example_liquid_glass/windows/runner/utils.cpp new file mode 100644 index 00000000..3a0b4651 --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/apps/example_liquid_glass/windows/runner/utils.h b/apps/example_liquid_glass/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/apps/example_liquid_glass/windows/runner/win32_window.cpp b/apps/example_liquid_glass/windows/runner/win32_window.cpp new file mode 100644 index 00000000..60608d0f --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/apps/example_liquid_glass/windows/runner/win32_window.h b/apps/example_liquid_glass/windows/runner/win32_window.h new file mode 100644 index 00000000..e901dde6 --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/ap_common_flutter_ui/lib/ap_common_flutter_ui.dart b/packages/ap_common_flutter_ui/lib/ap_common_flutter_ui.dart index f8659031..6cfec09e 100644 --- a/packages/ap_common_flutter_ui/lib/ap_common_flutter_ui.dart +++ b/packages/ap_common_flutter_ui/lib/ap_common_flutter_ui.dart @@ -27,6 +27,7 @@ export 'src/widgets/ap_drawer.dart'; export 'src/widgets/ap_network_image.dart'; export 'src/widgets/color_picker_widgets.dart'; export 'src/widgets/course_edit_sheet.dart'; +export 'src/widgets/course_table_view.dart'; export 'src/widgets/default_dialog.dart'; export 'src/widgets/dialog_option.dart'; export 'src/widgets/hint_banner.dart'; @@ -40,6 +41,7 @@ export 'src/widgets/option_picker_bottom_sheet.dart'; export 'src/widgets/phone_list_item.dart'; export 'src/widgets/progress_dialog.dart'; export 'src/widgets/score_analysis_widgets.dart'; +export 'src/widgets/score_tab_widgets.dart'; export 'src/widgets/semester_picker.dart'; export 'src/widgets/setting_page_widgets.dart'; export 'src/widgets/yes_no_dialog.dart'; diff --git a/packages/ap_common_flutter_ui/lib/src/scaffold/course_scaffold.dart b/packages/ap_common_flutter_ui/lib/src/scaffold/course_scaffold.dart index 12fdf9a8..cdd79eac 100644 --- a/packages/ap_common_flutter_ui/lib/src/scaffold/course_scaffold.dart +++ b/packages/ap_common_flutter_ui/lib/src/scaffold/course_scaffold.dart @@ -15,10 +15,6 @@ enum CourseState { loading, finish, error, empty, offlineEmpty, custom } enum CourseNotifyState { schedule, cancel } -enum _ContentStyle { list, table } - -const double _courseHeight = 64.0; - const String _kCourseInvisibleKey = '${ApConstants.packageName}.course_invisible_'; @@ -190,7 +186,7 @@ class CourseScaffold extends StatefulWidget { class CourseScaffoldState extends State { final GlobalKey _repaintBoundaryGlobalKey = GlobalKey(); - _ContentStyle _contentStyle = _ContentStyle.table; + ContentStyle _contentStyle = ContentStyle.table; bool? showSectionTime; bool? showInstructors; @@ -204,8 +200,6 @@ class CourseScaffoldState extends State { List invisibleCourseCodes = []; - late Map> _courseLookup; - final Map _courseColorMap = {}; int _colorIndex = 0; @@ -228,7 +222,6 @@ class CourseScaffoldState extends State { @override void initState() { - _buildCourseLookup(); showSectionTime = widget.showSectionTime ?? PreferenceUtil.instance.getBool(ApConstants.showSectionTime, true); showInstructors = widget.showInstructors ?? @@ -264,7 +257,6 @@ class CourseScaffoldState extends State { @override void didUpdateWidget(covariant CourseScaffold oldWidget) { if (widget.courseData != oldWidget.courseData) { - _buildCourseLookup(); _courseColorMap.clear(); _colorIndex = 0; } @@ -450,13 +442,13 @@ class CourseScaffoldState extends State { onPressed: () { setState( () => _contentStyle = - (_contentStyle == _ContentStyle.table) - ? _ContentStyle.list - : _ContentStyle.table, + (_contentStyle == ContentStyle.table) + ? ContentStyle.list + : ContentStyle.table, ); }, child: Icon( - _contentStyle == _ContentStyle.table + _contentStyle == ContentStyle.table ? Icons.list_rounded : Icons.grid_view_rounded, ), @@ -590,31 +582,26 @@ class CourseScaffoldState extends State { Icons.warning_amber_rounded, ); default: - if (isLandscape || _contentStyle == _ContentStyle.table) { - final int weekdayCount = widget.courseData.hasHoliday ? 7 : 5; - return SingleChildScrollView( + if (isLandscape || _contentStyle == ContentStyle.table) { + return CourseTableView( + courseData: widget.courseData, + invisibleCourseCodes: invisibleCourseCodes, controller: _scrollController, - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.only( - top: 8.0, - bottom: 80.0, - ), - child: RepaintBoundary( - key: _repaintBoundaryGlobalKey, - child: ColoredBox( - color: Theme.of(context).scaffoldBackgroundColor, - child: Column( - children: [ - _buildWeekdayHeader(colorScheme, weekdayCount), - _buildCourseGrid( - colorScheme, - weekdayCount, - widget.courseData.timeCodes, - ), - ], - ), - ), - ), + onCoursePressed: ( + Course course, + TimeCode timeCode, + int weekday, + ) => + _onPressed(weekday, timeCode, course), + onEmptyCellPressed: widget.enableCustomCourse + ? _addCustomCourse + : null, + courseColorResolver: _getCourseColor, + repaintBoundaryKey: _repaintBoundaryGlobalKey, + mergeCourse: mergeCourse, + showSectionTime: showSectionTime, + showInstructors: showInstructors, + showClassroomLocation: showClassroomLocation, ); } else { return CourseList( @@ -673,371 +660,6 @@ class CourseScaffoldState extends State { } } - Widget _buildWeekdayHeader(ColorScheme colorScheme, int weekdayCount) { - return Container( - decoration: BoxDecoration( - color: colorScheme.primaryContainer.withAlpha(77), - border: Border( - bottom: BorderSide( - color: colorScheme.outlineVariant.withAlpha(128), - ), - ), - ), - child: Row( - children: [ - _buildTimeSlotHeader(colorScheme), - for (int i = 0; i < weekdayCount; i++) - Expanded( - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Text( - context.ap.weekdaysCourse[i], - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: colorScheme.primary, - ), - ), - ), - ), - ], - ), - ); - } - - Widget _buildTimeSlotHeader(ColorScheme colorScheme) { - return Container( - width: 48, - padding: const EdgeInsets.symmetric(vertical: 12), - child: Text( - '', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: colorScheme.primary, - ), - ), - ); - } - - Widget _buildCourseGrid( - ColorScheme colorScheme, - int weekdayCount, - List timeCodes, - ) { - final int minIndex = widget.courseData.minTimeCodeIndex; - final int maxIndex = widget.courseData.maxTimeCodeIndex; - - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildTimeColumn(colorScheme, timeCodes, minIndex, maxIndex), - for (int weekday = 1; weekday <= weekdayCount; weekday++) - Expanded( - child: _buildWeekdayColumn( - colorScheme, - weekday, - weekdayCount, - minIndex, - maxIndex, - ), - ), - ], - ); - } - - Widget _buildTimeColumn( - ColorScheme colorScheme, - List timeCodes, - int minIndex, - int maxIndex, - ) { - return Column( - children: [ - for (int i = minIndex; i <= maxIndex; i++) - _buildTimeSlot( - colorScheme, - i < timeCodes.length ? timeCodes[i] : null, - i, - i == maxIndex, - ), - ], - ); - } - - Widget _buildWeekdayColumn( - ColorScheme colorScheme, - int weekday, - int weekdayCount, - int minIndex, - int maxIndex, - ) { - final List children = []; - for (int i = minIndex; i <= maxIndex; i++) { - final Course? course = _getCourseAt(weekday, i); - final bool isInvisible = - course != null && invisibleCourseCodes.contains(course.code); - - if (course == null || isInvisible) { - children.add( - _buildEmptyCell( - colorScheme, - weekday < weekdayCount, - i == maxIndex, - weekday: weekday, - timeIndex: i, - ), - ); - } else { - int span = 1; - if (mergeCourse ?? true) { - while (i + span <= maxIndex && - _getCourseAt(weekday, i + span) == course) { - span++; - } - } - children.add( - _buildCourseCell( - colorScheme, - weekday, - i, - course, - span, - weekday < weekdayCount, - i + span - 1 == maxIndex, - ), - ); - i += span - 1; - } - } - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: children, - ); - } - - Widget _buildTimeSlot( - ColorScheme colorScheme, - TimeCode? timeCode, - int timeIndex, - bool isLast, - ) { - return Container( - width: 48, - height: _courseHeight, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest.withAlpha(77), - border: Border( - right: BorderSide( - color: colorScheme.outlineVariant.withAlpha(128), - ), - bottom: isLast - ? BorderSide.none - : BorderSide( - color: colorScheme.outlineVariant.withAlpha(77), - width: 0.5, - ), - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - timeCode?.title ?? '${timeIndex + 1}', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: colorScheme.onSurfaceVariant, - ), - ), - if (timeCode != null && (showSectionTime ?? true)) ...[ - const SizedBox(height: 2), - Text( - timeCode.startTime, - style: TextStyle( - fontSize: 9, - color: colorScheme.onSurfaceVariant.withAlpha(179), - ), - ), - ], - ], - ), - ); - } - - Widget _buildEmptyCell( - ColorScheme colorScheme, - bool showRightBorder, - bool isLast, { - required int weekday, - required int timeIndex, - }) { - final bool canAdd = widget.enableCustomCourse; - - return GestureDetector( - onTap: canAdd ? () => _addCustomCourse(weekday, timeIndex) : null, - child: Container( - height: _courseHeight, - decoration: BoxDecoration( - border: Border( - right: showRightBorder - ? BorderSide( - color: colorScheme.outlineVariant.withAlpha(51), - width: 0.5, - ) - : BorderSide.none, - bottom: isLast - ? BorderSide.none - : BorderSide( - color: colorScheme.outlineVariant.withAlpha(77), - width: 0.5, - ), - ), - ), - ), - ); - } - - Widget _buildCourseCell( - ColorScheme colorScheme, - int weekday, - int timeIndex, - Course course, - int span, - bool showRightBorder, - bool isLast, - ) { - return Container( - height: _courseHeight * span, - decoration: BoxDecoration( - border: Border( - right: showRightBorder - ? BorderSide( - color: colorScheme.outlineVariant.withAlpha(51), - width: 0.5, - ) - : BorderSide.none, - bottom: isLast - ? BorderSide.none - : BorderSide( - color: colorScheme.outlineVariant.withAlpha(77), - width: 0.5, - ), - ), - ), - child: _buildCourseCard(colorScheme, weekday, timeIndex, course, span), - ); - } - - void _buildCourseLookup() { - _courseLookup = >{}; - for (final Course course in widget.courseData.courses) { - for (final SectionTime time in course.times) { - _courseLookup.putIfAbsent( - time.weekday, - () => {}, - )[time.index] = course; - } - } - } - - Course? _getCourseAt(int weekday, int timeIndex) { - return _courseLookup[weekday]?[timeIndex]; - } - - Widget _buildCourseCard( - ColorScheme colorScheme, - int weekday, - int timeIndex, - Course course, - int span, - ) { - final Color courseColor = _getCourseColor(course); - final String locationInfo = - (showClassroomLocation ?? true) && course.location != null - ? course.location.toString() - : ''; - final String instructorInfo = - (showInstructors ?? true) ? course.getInstructors() : ''; - - final Color onCourseColor = - ThemeData.estimateBrightnessForColor(courseColor) == Brightness.dark - ? Colors.white - : Colors.black; - - final String displayInfo = [ - if (instructorInfo.isNotEmpty) instructorInfo, - if (locationInfo.isNotEmpty) locationInfo, - ].join('\n'); - - return GestureDetector( - onTap: () { - final List timeCodes = widget.courseData.timeCodes; - final TimeCode startTimeCode = timeIndex < timeCodes.length - ? timeCodes[timeIndex] - : const TimeCode(title: '?', startTime: '?', endTime: '?'); - final int lastIndex = timeIndex + span - 1; - final TimeCode endTimeCode = - lastIndex < timeCodes.length ? timeCodes[lastIndex] : startTimeCode; - final TimeCode timeCode = startTimeCode.copyWith( - endTime: endTimeCode.endTime, - ); - _onPressed(weekday, timeCode, course); - }, - child: Container( - width: double.infinity, - height: double.infinity, - margin: const EdgeInsets.all(2), - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6), - decoration: BoxDecoration( - color: courseColor.withAlpha(230), - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: courseColor.withAlpha(77), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - course.title, - textAlign: TextAlign.center, - maxLines: span > 1 ? 4 : 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: span > 1 ? 14 : 9.5, - fontWeight: FontWeight.w600, - color: onCourseColor, - height: 1.1, - ), - ), - if (displayInfo.isNotEmpty) ...[ - SizedBox(height: span > 1 ? 4 : 2), - Text( - displayInfo, - textAlign: TextAlign.center, - maxLines: span > 1 ? 4 : 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: span > 1 ? 12 : 8.5, - color: onCourseColor.withAlpha(217), - height: 1.0, - ), - ), - ], - ], - ), - ), - ); - } - void _onPressed(int weekday, TimeCode timeCode, Course course) { showModalBottomSheet( context: context, diff --git a/packages/ap_common_flutter_ui/lib/src/scaffold/score_scaffold.dart b/packages/ap_common_flutter_ui/lib/src/scaffold/score_scaffold.dart index c135874b..f848ad27 100644 --- a/packages/ap_common_flutter_ui/lib/src/scaffold/score_scaffold.dart +++ b/packages/ap_common_flutter_ui/lib/src/scaffold/score_scaffold.dart @@ -238,7 +238,7 @@ class ScoreScaffoldState extends State { elevation: 12.0, child: ColoredBox( color: Theme.of(context).colorScheme.surfaceContainerHighest, - child: _ScoreListTab( + child: ScoreListTab( scoreData: widget.scoreData!, onRefresh: widget.onRefresh, middleTitle: widget.middleTitle, @@ -457,13 +457,13 @@ class _ScoreContentState extends State { if (widget.scoreData == null) return const SizedBox.shrink(); if (widget.isAnalysisView) { - return _ScoreAnalysisTab( + return ScoreAnalysisTab( scoreData: widget.scoreData!, onRefresh: widget.onRefresh, controller: widget.scrollController, ); } else { - return _ScoreListTab( + return ScoreListTab( scoreData: widget.scoreData!, onRefresh: widget.onRefresh, middleTitle: widget.middleTitle, @@ -477,471 +477,13 @@ class _ScoreContentState extends State { } } -class _ScoreListTab extends StatelessWidget { - const _ScoreListTab({ - required this.scoreData, - this.onRefresh, - this.middleTitle, - this.finalTitle, - this.onScoreSelect, - this.middleScoreBuilder, - this.finalScoreBuilder, - this.controller, - }); - - final ScoreData scoreData; - final VoidCallback? onRefresh; - final String? middleTitle; - final String? finalTitle; - final Function(int index)? onScoreSelect; - final Widget Function(int index)? middleScoreBuilder; - final Widget Function(int index)? finalScoreBuilder; - final ScrollController? controller; - - @override - Widget build(BuildContext context) { - final ColorScheme colorScheme = Theme.of(context).colorScheme; - - return RefreshIndicator( - onRefresh: () async => onRefresh?.call(), - child: ListView.builder( - controller: controller, - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.all(16), - itemCount: scoreData.scores.length, - itemBuilder: (BuildContext context, int index) { - return _buildScoreItem( - context, - colorScheme, - scoreData.scores[index], - index, - ); - }, - ), - ); - } - - Widget _buildScoreItem( - BuildContext context, - ColorScheme colorScheme, - Score score, - int index, - ) { - final String? effectiveStr = _effectiveScoreStr(score); - final double? scoreValue = _parseScore(effectiveStr); - final bool isPassed = scoreValue != null && - (scoreData.scoreType == ScoreType.gradePoint - ? ScoreAnalysis.scoreToGradePoint(scoreValue) >= - scoreData.passingGradePoint - : scoreValue >= scoreData.passingScore); - final Color scoreColor = scoreValue == null - ? colorScheme.onSurfaceVariant - : isPassed - ? _getScoreColor(scoreValue) - : colorScheme.error; - - return GestureDetector( - onTap: onScoreSelect == null - ? null - : () { - onScoreSelect!(index); - AnalyticsUtil.instance.logEvent('score_title_click'); - }, - child: Container( - margin: const EdgeInsets.only(bottom: 8), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerLowest, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: colorScheme.outlineVariant.withAlpha(77)), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Container( - width: 4, - height: 50, - decoration: BoxDecoration( - color: scoreColor, - borderRadius: BorderRadius.circular(2), - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - score.title, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, - ), - ), - const SizedBox(height: 4), - Row( - children: [ - _buildTag( - colorScheme, - score.required ?? '', - colorScheme.tertiary, - ), - const SizedBox(width: 8), - Text( - context.ap.unitCountFormat( - arg1: score.units, - ), - style: TextStyle( - fontSize: 12, - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ], - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (finalScoreBuilder == null) - ..._buildScoreDisplay( - colorScheme, - score, - scoreValue, - scoreColor, - ), - if (finalScoreBuilder != null) finalScoreBuilder!(index), - const SizedBox(height: 4), - if (middleScoreBuilder == null && - score.middleScore != null && - score.middleScore!.isNotEmpty) - Text( - context.ap.midtermPrefix( - arg1: score.middleScore!, - ), - style: TextStyle( - fontSize: 11, - color: colorScheme.onSurfaceVariant, - ), - ), - if (middleScoreBuilder != null) middleScoreBuilder!(index), - ], - ), - ], - ), - ), - ), - ); - } - - /// Build the score display based on data type. - /// - Numeric scores (e.g. "90") → show number + converted letter grade - /// - Letter grades (e.g. "A+") → show letter + converted grade point - List _buildScoreDisplay( - ColorScheme colorScheme, - Score score, - double? scoreValue, - Color scoreColor, - ) { - final String raw = _effectiveScoreStr(score) ?? '-'; - final bool isNumeric = - scoreData.scoreType == ScoreType.numeric; - - if (scoreValue == null) { - // Cannot parse at all — show raw string - return [ - Text( - raw, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: scoreColor, - ), - ), - ]; - } - - if (isNumeric) { - // Data source is numeric → show number as primary, - // letter grade as secondary - return [ - Text( - raw, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: scoreColor, - ), - ), - Text( - ScoreAnalysis.scoreToGradeLetter(scoreValue), - style: TextStyle( - fontSize: 11, - color: colorScheme.onSurfaceVariant, - ), - ), - ]; - } else { - // Data source is letter grade → show letter as primary, - // grade point as secondary - return [ - Text( - raw, - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: scoreColor, - ), - ), - Text( - ScoreAnalysis.scoreToGradePoint(scoreValue).toStringAsFixed(1), - style: TextStyle( - fontSize: 11, - color: colorScheme.onSurfaceVariant, - ), - ), - ]; - } - } - - Color _getScoreColor(double score) { - if (score >= 90) return const Color(0xFF4CAF50); - if (score >= 80) return const Color(0xFF8BC34A); - if (score >= 70) return const Color(0xFF2196F3); - if (score >= 60) return const Color(0xFFFF9800); - return const Color(0xFFF44336); - } - - /// Delegates to [ScoreAnalysis.effectiveScoreStr]. - static String? _effectiveScoreStr(Score score) => - ScoreAnalysis.effectiveScoreStr(score); - - /// Delegates to [ScoreAnalysis.parseScore]. - static double? _parseScore(String? scoreStr) => - ScoreAnalysis.parseScore(scoreStr); - - Widget _buildTag(ColorScheme colorScheme, String text, Color color) { - if (text.isEmpty) return const SizedBox.shrink(); - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: color.withAlpha(26), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - text.replaceAll('【', '').replaceAll('】', ''), - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: color, - ), - ), - ); - } -} - -class _ScoreAnalysisTab extends StatelessWidget { - const _ScoreAnalysisTab({ - required this.scoreData, - this.onRefresh, - this.controller, - }); - - final ScoreData scoreData; - final VoidCallback? onRefresh; - final ScrollController? controller; - - @override - Widget build(BuildContext context) { - final ColorScheme colorScheme = Theme.of(context).colorScheme; - final ScoreAnalysis analysis = ScoreAnalysis(scoreData); - - return RefreshIndicator( - onRefresh: () async => onRefresh?.call(), - child: SingleChildScrollView( - controller: controller, - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.all(16), - child: Column( - children: [ - _buildMainSummaryCard(colorScheme, context.ap, analysis), - const SizedBox(height: 16), - ScorePRCard(analysis: analysis), - const SizedBox(height: 16), - ScoreGPACard(analysis: analysis), - const SizedBox(height: 16), - ScoreStatisticsCard(analysis: analysis), - const SizedBox(height: 16), - ScoreDistributionCard(analysis: analysis), - const SizedBox(height: 16), - ScoreCreditSummaryCard(analysis: analysis), - const SizedBox(height: 32), - ], - ), - ), - ); - } - - Widget _buildMainSummaryCard( - ColorScheme colorScheme, - ApLocalizations ap, - ScoreAnalysis analysis, - ) { - final Detail detail = scoreData.detail; - - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - colorScheme.primaryContainer, - colorScheme.secondaryContainer, - ], - ), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: colorScheme.primary.withAlpha(38), - blurRadius: 16, - offset: const Offset(0, 8), - ), - ], - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(20), - child: Row( - children: [ - Expanded( - child: _buildMainItem( - colorScheme, - Icons.star_rounded, - ap.average, - detail.average?.toStringAsFixed(2) ?? '-', - ), - ), - if (detail.conduct != null) ...[ - Container( - width: 1, - height: 60, - color: colorScheme.onPrimaryContainer.withAlpha(51), - ), - Expanded( - child: _buildMainItem( - colorScheme, - Icons.school_rounded, - ap.conductScore, - detail.conduct!.toStringAsFixed(0), - ), - ), - ], - ], - ), - ), - if (detail.classRank != null || detail.departmentRank != null) - Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - decoration: BoxDecoration( - color: colorScheme.surface.withAlpha(179), - borderRadius: const BorderRadius.vertical( - bottom: Radius.circular(20), - ), - ), - child: Row( - children: [ - if (detail.classRank != null) - Expanded( - child: _buildRankItem( - colorScheme, - ap.classRank, - detail.classRank!, - ), - ), - if (detail.classRank != null && detail.departmentRank != null) - Container( - width: 1, - height: 40, - color: colorScheme.outlineVariant.withAlpha(128), - ), - if (detail.departmentRank != null) - Expanded( - child: _buildRankItem( - colorScheme, - ap.departmentRank, - detail.departmentRank!, - ), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildMainItem( - ColorScheme colorScheme, - IconData icon, - String label, - String value, - ) { - return Column( - children: [ - Icon(icon, size: 28, color: colorScheme.primary), - const SizedBox(height: 8), - Text( - value, - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: colorScheme.onPrimaryContainer, - ), - ), - const SizedBox(height: 4), - Text( - label, - style: TextStyle( - fontSize: 13, - color: colorScheme.onPrimaryContainer.withAlpha(179), - ), - ), - ], - ); - } - - Widget _buildRankItem(ColorScheme colorScheme, String label, String value) { - return Column( - children: [ - Text( - value, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - ), - const SizedBox(height: 2), - Text( - label, - style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant), - ), - ], - ); - } -} - class ScoreAnalysis { ScoreAnalysis(this.scoreData) { _scores = []; _gradePoints = []; for (final Score score in scoreData.scores) { - final double? value = _ScoreListTab._parseScore( - _ScoreListTab._effectiveScoreStr(score), + final double? value = ScoreAnalysis.parseScore( + ScoreAnalysis.effectiveScoreStr(score), ); if (value != null) { _scores.add(value); @@ -981,8 +523,8 @@ class ScoreAnalysis { double totalWeighted = 0; double totalUnits = 0; for (final Score score in scoreData.scores) { - final double? value = _ScoreListTab._parseScore( - _ScoreListTab._effectiveScoreStr(score), + final double? value = ScoreAnalysis.parseScore( + ScoreAnalysis.effectiveScoreStr(score), ); final double? unit = double.tryParse(score.units); if (value != null && unit != null && unit > 0) { @@ -1083,8 +625,8 @@ class ScoreAnalysis { if (detailCredits != null && detailCredits > 0) return detailCredits; double credits = 0; for (final Score score in scoreData.scores) { - final double? scoreValue = _ScoreListTab._parseScore( - _ScoreListTab._effectiveScoreStr(score), + final double? scoreValue = ScoreAnalysis.parseScore( + ScoreAnalysis.effectiveScoreStr(score), ); final double? unit = double.tryParse(score.units); if (scoreValue != null && @@ -1205,8 +747,8 @@ class ScoreAnalysis { double totalWeighted = 0; double totalUnits = 0; for (final Score score in scoreData.scores) { - final double? scoreValue = _ScoreListTab._parseScore( - _ScoreListTab._effectiveScoreStr(score), + final double? scoreValue = ScoreAnalysis.parseScore( + ScoreAnalysis.effectiveScoreStr(score), ); final double? unit = double.tryParse(score.units); if (scoreValue != null && unit != null && unit > 0) { diff --git a/packages/ap_common_flutter_ui/lib/src/widgets/course_table_view.dart b/packages/ap_common_flutter_ui/lib/src/widgets/course_table_view.dart new file mode 100644 index 00000000..506ed064 --- /dev/null +++ b/packages/ap_common_flutter_ui/lib/src/widgets/course_table_view.dart @@ -0,0 +1,523 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; + +/// The display style for course content. +enum ContentStyle { list, table } + +/// Height of each course time slot cell. +const double courseHeight = 64.0; + +/// Builds a lookup map from (weekday, timeIndex) to [Course]. +/// +/// Returns a nested map where the outer key is the weekday number +/// and the inner key is the time slot index. +Map> buildCourseLookup( + List courses, +) { + final Map> lookup = + >{}; + for (final Course course in courses) { + for (final SectionTime time in course.times) { + lookup + .putIfAbsent( + time.weekday, + () => {}, + )[time.index] = course; + } + } + return lookup; +} + +/// A widget that renders a course timetable grid. +/// +/// This widget encapsulates all the grid rendering logic for +/// displaying courses in a table layout, including weekday headers, +/// time slot columns, and course cards. +class CourseTableView extends StatelessWidget { + /// Creates a [CourseTableView]. + const CourseTableView({ + super.key, + required this.courseData, + required this.invisibleCourseCodes, + this.controller, + this.onCoursePressed, + this.onEmptyCellPressed, + required this.courseColorResolver, + this.repaintBoundaryKey, + this.mergeCourse, + this.showSectionTime, + this.showInstructors, + this.showClassroomLocation, + }); + + /// The course data to display. + final CourseData courseData; + + /// Course codes that should be hidden in the grid. + final List invisibleCourseCodes; + + /// Optional scroll controller for the grid. + final ScrollController? controller; + + /// Callback when a course cell is tapped. + final void Function( + Course course, + TimeCode timeCode, + int weekday, + )? onCoursePressed; + + /// Callback when an empty cell is tapped (e.g. for adding + /// a custom course). + final void Function(int weekday, int timeIndex)? + onEmptyCellPressed; + + /// Resolves a [Course] to its display color. + final Color Function(Course course) courseColorResolver; + + /// Key for the [RepaintBoundary] used for screenshot capture. + final GlobalKey? repaintBoundaryKey; + + /// Whether to merge adjacent identical course cells. + final bool? mergeCourse; + + /// Whether to show time information in time slot cells. + final bool? showSectionTime; + + /// Whether to show instructor names on course cards. + final bool? showInstructors; + + /// Whether to show classroom location on course cards. + final bool? showClassroomLocation; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final int weekdayCount = courseData.hasHoliday ? 7 : 5; + final Map> courseLookup = + buildCourseLookup(courseData.courses); + return SingleChildScrollView( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.only( + top: 8.0, + bottom: 80.0, + ), + child: RepaintBoundary( + key: repaintBoundaryKey, + child: ColoredBox( + color: Theme.of(context).scaffoldBackgroundColor, + child: Column( + children: [ + _buildWeekdayHeader( + context, + colorScheme, + weekdayCount, + ), + _buildCourseGrid( + context, + colorScheme, + weekdayCount, + courseData.timeCodes, + courseLookup, + ), + ], + ), + ), + ), + ); + } + + Widget _buildWeekdayHeader( + BuildContext context, + ColorScheme colorScheme, + int weekdayCount, + ) { + return Container( + decoration: BoxDecoration( + color: colorScheme.primaryContainer.withAlpha(77), + border: Border( + bottom: BorderSide( + color: + colorScheme.outlineVariant.withAlpha(128), + ), + ), + ), + child: Row( + children: [ + _buildTimeSlotHeader(colorScheme), + for (int i = 0; i < weekdayCount; i++) + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + child: Text( + context.ap.weekdaysCourse[i], + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: colorScheme.primary, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildTimeSlotHeader(ColorScheme colorScheme) { + return Container( + width: 48, + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + '', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: colorScheme.primary, + ), + ), + ); + } + + Widget _buildCourseGrid( + BuildContext context, + ColorScheme colorScheme, + int weekdayCount, + List timeCodes, + Map> courseLookup, + ) { + final int minIndex = courseData.minTimeCodeIndex; + final int maxIndex = courseData.maxTimeCodeIndex; + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTimeColumn( + colorScheme, + timeCodes, + minIndex, + maxIndex, + ), + for (int weekday = 1; + weekday <= weekdayCount; + weekday++) + Expanded( + child: _buildWeekdayColumn( + colorScheme, + weekday, + weekdayCount, + minIndex, + maxIndex, + courseLookup, + ), + ), + ], + ); + } + + Widget _buildTimeColumn( + ColorScheme colorScheme, + List timeCodes, + int minIndex, + int maxIndex, + ) { + return Column( + children: [ + for (int i = minIndex; i <= maxIndex; i++) + _buildTimeSlot( + colorScheme, + i < timeCodes.length ? timeCodes[i] : null, + i, + i == maxIndex, + ), + ], + ); + } + + Widget _buildWeekdayColumn( + ColorScheme colorScheme, + int weekday, + int weekdayCount, + int minIndex, + int maxIndex, + Map> courseLookup, + ) { + final List children = []; + for (int i = minIndex; i <= maxIndex; i++) { + final Course? course = + courseLookup[weekday]?[i]; + final bool isInvisible = course != null && + invisibleCourseCodes.contains(course.code); + + if (course == null || isInvisible) { + children.add( + _buildEmptyCell( + colorScheme, + weekday < weekdayCount, + i == maxIndex, + weekday: weekday, + timeIndex: i, + ), + ); + } else { + int span = 1; + if (mergeCourse ?? true) { + while (i + span <= maxIndex && + courseLookup[weekday]?[i + span] == + course) { + span++; + } + } + children.add( + _buildCourseCell( + colorScheme, + weekday, + i, + course, + span, + weekday < weekdayCount, + i + span - 1 == maxIndex, + ), + ); + i += span - 1; + } + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, + ); + } + + Widget _buildTimeSlot( + ColorScheme colorScheme, + TimeCode? timeCode, + int timeIndex, + bool isLast, + ) { + return Container( + width: 48, + height: courseHeight, + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 4, + ), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest + .withAlpha(77), + border: Border( + right: BorderSide( + color: + colorScheme.outlineVariant.withAlpha(128), + ), + bottom: isLast + ? BorderSide.none + : BorderSide( + color: colorScheme.outlineVariant + .withAlpha(77), + width: 0.5, + ), + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + timeCode?.title ?? '${timeIndex + 1}', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: colorScheme.onSurfaceVariant, + ), + ), + if (timeCode != null && + (showSectionTime ?? true)) ...[ + const SizedBox(height: 2), + Text( + timeCode.startTime, + style: TextStyle( + fontSize: 9, + color: colorScheme.onSurfaceVariant + .withAlpha(179), + ), + ), + ], + ], + ), + ); + } + + Widget _buildEmptyCell( + ColorScheme colorScheme, + bool showRightBorder, + bool isLast, { + required int weekday, + required int timeIndex, + }) { + return GestureDetector( + onTap: onEmptyCellPressed != null + ? () => onEmptyCellPressed!(weekday, timeIndex) + : null, + child: Container( + height: courseHeight, + decoration: BoxDecoration( + border: Border( + right: showRightBorder + ? BorderSide( + color: colorScheme.outlineVariant + .withAlpha(51), + width: 0.5, + ) + : BorderSide.none, + bottom: isLast + ? BorderSide.none + : BorderSide( + color: colorScheme.outlineVariant + .withAlpha(77), + width: 0.5, + ), + ), + ), + ), + ); + } + + Widget _buildCourseCell( + ColorScheme colorScheme, + int weekday, + int timeIndex, + Course course, + int span, + bool showRightBorder, + bool isLast, + ) { + return Container( + height: courseHeight * span, + decoration: BoxDecoration( + border: Border( + right: showRightBorder + ? BorderSide( + color: colorScheme.outlineVariant + .withAlpha(51), + width: 0.5, + ) + : BorderSide.none, + bottom: isLast + ? BorderSide.none + : BorderSide( + color: colorScheme.outlineVariant + .withAlpha(77), + width: 0.5, + ), + ), + ), + child: _buildCourseCard( + colorScheme, + weekday, + timeIndex, + course, + span, + ), + ); + } + + Widget _buildCourseCard( + ColorScheme colorScheme, + int weekday, + int timeIndex, + Course course, + int span, + ) { + final Color courseColor = + courseColorResolver(course); + final String locationInfo = + (showClassroomLocation ?? true) && + course.location != null + ? course.location.toString() + : ''; + final String instructorInfo = + (showInstructors ?? true) + ? course.getInstructors() + : ''; + + final Color onCourseColor = + ThemeData.estimateBrightnessForColor(courseColor) == + Brightness.dark + ? Colors.white + : Colors.black; + + final String displayInfo = [ + if (instructorInfo.isNotEmpty) instructorInfo, + if (locationInfo.isNotEmpty) locationInfo, + ].join('\n'); + + return GestureDetector( + onTap: () { + final TimeCode timeCode = + timeIndex < courseData.timeCodes.length + ? courseData.timeCodes[timeIndex] + : const TimeCode( + title: '?', + startTime: '?', + endTime: '?', + ); + onCoursePressed?.call(course, timeCode, weekday); + }, + child: Container( + width: double.infinity, + height: double.infinity, + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 6, + ), + decoration: BoxDecoration( + color: courseColor.withAlpha(230), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: courseColor.withAlpha(77), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + course.title, + textAlign: TextAlign.center, + maxLines: span > 1 ? 4 : 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: span > 1 ? 14 : 9.5, + fontWeight: FontWeight.w600, + color: onCourseColor, + height: 1.1, + ), + ), + if (displayInfo.isNotEmpty) ...[ + SizedBox(height: span > 1 ? 4 : 2), + Text( + displayInfo, + textAlign: TextAlign.center, + maxLines: span > 1 ? 4 : 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: span > 1 ? 12 : 8.5, + color: onCourseColor.withAlpha(217), + height: 1.0, + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/packages/ap_common_flutter_ui/lib/src/widgets/score_tab_widgets.dart b/packages/ap_common_flutter_ui/lib/src/widgets/score_tab_widgets.dart new file mode 100644 index 00000000..b8517f3c --- /dev/null +++ b/packages/ap_common_flutter_ui/lib/src/widgets/score_tab_widgets.dart @@ -0,0 +1,490 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; + +class ScoreListTab extends StatelessWidget { + const ScoreListTab({ + super.key, + required this.scoreData, + this.onRefresh, + this.middleTitle, + this.finalTitle, + this.onScoreSelect, + this.middleScoreBuilder, + this.finalScoreBuilder, + this.controller, + }); + + final ScoreData scoreData; + final VoidCallback? onRefresh; + final String? middleTitle; + final String? finalTitle; + final Function(int index)? onScoreSelect; + final Widget Function(int index)? middleScoreBuilder; + final Widget Function(int index)? finalScoreBuilder; + final ScrollController? controller; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + + return RefreshIndicator( + onRefresh: () async => onRefresh?.call(), + child: ListView.builder( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + itemCount: scoreData.scores.length, + itemBuilder: (BuildContext context, int index) { + return _buildScoreItem( + context, + colorScheme, + scoreData.scores[index], + index, + ); + }, + ), + ); + } + + Widget _buildScoreItem( + BuildContext context, + ColorScheme colorScheme, + Score score, + int index, + ) { + final String? effectiveStr = + ScoreAnalysis.effectiveScoreStr(score); + final double? scoreValue = + ScoreAnalysis.parseScore(effectiveStr); + final bool isPassed = scoreValue != null && + (scoreData.scoreType == ScoreType.gradePoint + ? ScoreAnalysis.scoreToGradePoint(scoreValue) >= + scoreData.passingGradePoint + : scoreValue >= scoreData.passingScore); + final Color scoreColor = scoreValue == null + ? colorScheme.onSurfaceVariant + : isPassed + ? _getScoreColor(scoreValue) + : colorScheme.error; + + return GestureDetector( + onTap: onScoreSelect == null + ? null + : () { + onScoreSelect!(index); + AnalyticsUtil.instance + .logEvent('score_title_click'); + }, + child: Container( + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerLowest, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: colorScheme.outlineVariant.withAlpha(77), + ), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + width: 4, + height: 50, + decoration: BoxDecoration( + color: scoreColor, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + score.title, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + _buildTag( + colorScheme, + score.required ?? '', + colorScheme.tertiary, + ), + const SizedBox(width: 8), + Text( + context.ap.unitCountFormat( + arg1: score.units, + ), + style: TextStyle( + fontSize: 12, + color: + colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (finalScoreBuilder == null) + ..._buildScoreDisplay( + colorScheme, + score, + scoreValue, + scoreColor, + ), + if (finalScoreBuilder != null) + finalScoreBuilder!(index), + const SizedBox(height: 4), + if (middleScoreBuilder == null && + score.middleScore != null && + score.middleScore!.isNotEmpty) + Text( + context.ap.midtermPrefix( + arg1: score.middleScore!, + ), + style: TextStyle( + fontSize: 11, + color: colorScheme.onSurfaceVariant, + ), + ), + if (middleScoreBuilder != null) + middleScoreBuilder!(index), + ], + ), + ], + ), + ), + ), + ); + } + + /// Build the score display based on data type. + /// - Numeric scores (e.g. "90") -> show number + + /// converted letter grade + /// - Letter grades (e.g. "A+") -> show letter + + /// converted grade point + List _buildScoreDisplay( + ColorScheme colorScheme, + Score score, + double? scoreValue, + Color scoreColor, + ) { + final String raw = + ScoreAnalysis.effectiveScoreStr(score) ?? '-'; + final bool isNumeric = + scoreData.scoreType == ScoreType.numeric; + + if (scoreValue == null) { + return [ + Text( + raw, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: scoreColor, + ), + ), + ]; + } + + if (isNumeric) { + return [ + Text( + raw, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: scoreColor, + ), + ), + Text( + ScoreAnalysis.scoreToGradeLetter(scoreValue), + style: TextStyle( + fontSize: 11, + color: colorScheme.onSurfaceVariant, + ), + ), + ]; + } else { + return [ + Text( + raw, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: scoreColor, + ), + ), + Text( + ScoreAnalysis.scoreToGradePoint(scoreValue) + .toStringAsFixed(1), + style: TextStyle( + fontSize: 11, + color: colorScheme.onSurfaceVariant, + ), + ), + ]; + } + } + + Color _getScoreColor(double score) { + if (score >= 90) return const Color(0xFF4CAF50); + if (score >= 80) return const Color(0xFF8BC34A); + if (score >= 70) return const Color(0xFF2196F3); + if (score >= 60) return const Color(0xFFFF9800); + return const Color(0xFFF44336); + } + + Widget _buildTag( + ColorScheme colorScheme, + String text, + Color color, + ) { + if (text.isEmpty) return const SizedBox.shrink(); + + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: color.withAlpha(26), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + text.replaceAll('【', '').replaceAll('】', ''), + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: color, + ), + ), + ); + } +} + +class ScoreAnalysisTab extends StatelessWidget { + const ScoreAnalysisTab({ + super.key, + required this.scoreData, + this.onRefresh, + this.controller, + }); + + final ScoreData scoreData; + final VoidCallback? onRefresh; + final ScrollController? controller; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final ScoreAnalysis analysis = ScoreAnalysis(scoreData); + + return RefreshIndicator( + onRefresh: () async => onRefresh?.call(), + child: SingleChildScrollView( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _buildMainSummaryCard( + colorScheme, + context.ap, + analysis, + ), + const SizedBox(height: 16), + ScorePRCard(analysis: analysis), + const SizedBox(height: 16), + ScoreGPACard(analysis: analysis), + const SizedBox(height: 16), + ScoreStatisticsCard(analysis: analysis), + const SizedBox(height: 16), + ScoreDistributionCard(analysis: analysis), + const SizedBox(height: 16), + ScoreCreditSummaryCard(analysis: analysis), + const SizedBox(height: 32), + ], + ), + ), + ); + } + + Widget _buildMainSummaryCard( + ColorScheme colorScheme, + ApLocalizations ap, + ScoreAnalysis analysis, + ) { + final Detail detail = scoreData.detail; + + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.primaryContainer, + colorScheme.secondaryContainer, + ], + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: colorScheme.primary.withAlpha(38), + blurRadius: 16, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Expanded( + child: _buildMainItem( + colorScheme, + Icons.star_rounded, + ap.average, + detail.average?.toStringAsFixed(2) ?? + '-', + ), + ), + if (detail.conduct != null) ...[ + Container( + width: 1, + height: 60, + color: colorScheme.onPrimaryContainer + .withAlpha(51), + ), + Expanded( + child: _buildMainItem( + colorScheme, + Icons.school_rounded, + ap.conductScore, + detail.conduct!.toStringAsFixed(0), + ), + ), + ], + ], + ), + ), + if (detail.classRank != null || + detail.departmentRank != null) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + decoration: BoxDecoration( + color: colorScheme.surface.withAlpha(179), + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(20), + ), + ), + child: Row( + children: [ + if (detail.classRank != null) + Expanded( + child: _buildRankItem( + colorScheme, + ap.classRank, + detail.classRank!, + ), + ), + if (detail.classRank != null && + detail.departmentRank != null) + Container( + width: 1, + height: 40, + color: colorScheme.outlineVariant + .withAlpha(128), + ), + if (detail.departmentRank != null) + Expanded( + child: _buildRankItem( + colorScheme, + ap.departmentRank, + detail.departmentRank!, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildMainItem( + ColorScheme colorScheme, + IconData icon, + String label, + String value, + ) { + return Column( + children: [ + Icon(icon, size: 28, color: colorScheme.primary), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: colorScheme.onPrimaryContainer, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + fontSize: 13, + color: colorScheme.onPrimaryContainer + .withAlpha(179), + ), + ), + ], + ); + } + + Widget _buildRankItem( + ColorScheme colorScheme, + String label, + String value, + ) { + return Column( + children: [ + Text( + value, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 2), + Text( + label, + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ); + } +} diff --git a/packages/ap_common_liquid_glass/CHANGELOG.md b/packages/ap_common_liquid_glass/CHANGELOG.md new file mode 100644 index 00000000..6febad8e --- /dev/null +++ b/packages/ap_common_liquid_glass/CHANGELOG.md @@ -0,0 +1,8 @@ +## 0.1.0-dev.1 + +- Initial development release +- `LiquidGlassApApp` wrapper with automatic shader initialization +- `GlassThemeBridge` for mapping ApTheme seed colors to GlassThemeData +- Glass scaffold variants for all 6 scaffolds +- Glass widget replacements (drawer, dialogs, buttons) +- Glass convenience pages diff --git a/packages/ap_common_liquid_glass/LIQUID_GLASS_SPEC.md b/packages/ap_common_liquid_glass/LIQUID_GLASS_SPEC.md new file mode 100644 index 00000000..3908937f --- /dev/null +++ b/packages/ap_common_liquid_glass/LIQUID_GLASS_SPEC.md @@ -0,0 +1,156 @@ +# Liquid Glass Integration Spec + +> This file captures the design decisions and implementation status from the +> initial planning session. Read this before continuing development. + +## Architecture Decision + +- **New package**: `packages/ap_common_liquid_glass/` (opt-in, not integrated into `ap_common_flutter_ui`) +- **Dependency**: `liquid_glass_widgets: ^0.7.0` (pre-1.0, API may change) +- **Scope**: All 6 scaffolds + drawer + dialogs + 3 convenience pages + +## Dependency Chain + +``` +ap_common_flutter_ui (Material 3, no glass dependency) + └─ ap_common_liquid_glass (opt-in glass layer) + └─ liquid_glass_widgets ^0.7.0 +``` + +## Current Implementation Status + +### Done (initial scaffold, needs `pub get` + `analyze` to verify) + +| File | Status | Notes | +|------|--------|-------| +| `pubspec.yaml` | Done | Registered in root workspace | +| `lib/ap_common_liquid_glass.dart` | Done | Barrel export | +| `lib/src/liquid_glass_app.dart` | Done | `LiquidGlassApApp` wrapping `MaterialApp` + `LiquidGlassWidgets.wrap()` + `GlassTheme` | +| `lib/src/glass_theme_bridge.dart` | Done | `GlassThemeBridge.fromContext()` — currently returns default `GlassThemeData`, needs seed color mapping | +| `lib/src/scaffold/glass_home_page_scaffold.dart` | Done | `GlassAppBar`, keeps Material `NavigationBar` (TODO: swap to `GlassBottomBar`) | +| `lib/src/scaffold/glass_login_scaffold.dart` | Done | Gradient background kept, form card → `GlassCard`, logo image → `GlassCard` | +| `lib/src/scaffold/glass_course_scaffold.dart` | Done | Delegates to original `CourseScaffold` (complex internal state) | +| `lib/src/scaffold/glass_score_scaffold.dart` | Done | Delegates to original `ScoreScaffold` | +| `lib/src/scaffold/glass_user_info_scaffold.dart` | Done | Needs review — SliverAppBar has no direct glass equivalent | +| `lib/src/scaffold/glass_image_viewer_scaffold.dart` | Done | `GlassAppBar` + `GlassTabBar` | +| `lib/src/widgets/glass_ap_drawer.dart` | Done | Needs review | +| `lib/src/widgets/glass_dialog.dart` | Done | `GlassYesNoDialog`, `GlassDefaultDialog` | +| `lib/src/pages/glass_ap_login_page.dart` | Done | Wraps `GlassLoginScaffold` | +| `lib/src/pages/glass_ap_course_page.dart` | Done | Wraps `GlassCourseScaffold` | +| `lib/src/pages/glass_ap_score_page.dart` | Done | Wraps `GlassScoreScaffold` | + +### TODO (next steps) + +1. **`flutter pub get` + `dart analyze`** — fix all compile errors against real `liquid_glass_widgets` API +2. **GlassThemeBridge** — map `ApTheme.seedColor` to glass tint color (currently returns defaults) +3. **GlassBottomBar** — `GlassHomePageScaffold` still uses Material `NavigationBar`; need to map `List bottomNavigationBarItems` to `GlassBottomBar` tabs +4. **CourseScaffold / ScoreScaffold deep integration** — currently just delegates to original; ideally: + - Extract body builder from original scaffolds + - Replace AppBar, FAB, card surfaces with glass equivalents +5. **UserInfoScaffold** — `SliverAppBar` with gradient has no glass equivalent; design custom glass header +6. **Example App** — add glass demo page in `apps/example/` +7. **Widget tests** — at least smoke test each glass scaffold renders without errors +8. **`AboutUsPage` glass variant** — not yet created + +## Key Design Decisions + +### Composition over Duplication +- Glass scaffolds should **reuse** original scaffold logic, not copy-paste +- Replace only the "chrome" (AppBar, NavigationBar, FAB, Card surfaces) +- Keep all state management, DataState patterns, responsive layouts + +### API Compatibility +- Glass scaffolds accept the **same parameters** as originals +- Downstream apps swap `HomePageScaffold(...)` → `GlassHomePageScaffold(...)` with no other changes +- `LiquidGlassApApp` has the same API as `ApApp` + `quality` and `glassSettings` params + +### Initialization +```dart +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await LiquidGlassWidgets.initialize(); // pre-cache shaders + registerOneForAll(); + registerApCommonService(); + await (PreferenceUtil.instance as ApPreferenceUtil).init(key: key, iv: iv); + runApp(const MyApp()); +} + +// Use LiquidGlassApApp instead of ApApp +LiquidGlassApApp( + home: GlassHomePage(), + quality: GlassQuality.standard, // or .premium for Impeller +) +``` + +## Widget Mapping Reference + +| liquid_glass_widgets | Replaces in ap_common | Where Used | +|---------------------|----------------------|------------| +| `GlassAppBar` | `AppBar` | All scaffolds | +| `GlassBottomBar` | `NavigationBar` | HomePageScaffold | +| `GlassCard` | Card surfaces, form containers | LoginScaffold, settings | +| `GlassButton` | `FloatingActionButton`, `ApButton` | Course/Score FABs | +| `GlassTextField` | `ApTextField` | Login form | +| `GlassDialog` | `DefaultDialog`, `YesNoDialog` | Dialogs | +| `GlassSideBar` | `Drawer` in `ApDrawer` | Drawer | +| `GlassTabBar` | `TabBar` | ImageViewerScaffold | +| `GlassSearchBar` | Search in CourseScaffold | Course search | +| `GlassListTile` | Course/Score list items | List views | +| `GlassSheet` | Bottom sheets | Various | +| `GlassSnackBar` | SnackBar | Hints/notifications | +| `GlassTheme` + `GlassThemeData` | — | App-level via `GlassThemeBridge` | + +## Original Scaffold Structures (for reference) + +### HomePageScaffold +- `AppBar` (title + actions) → `GlassAppBar` +- `NavigationBar` (elevation 12, height 56) → `GlassBottomBar` +- Announcement carousel with auto-play `PageView` +- Responsive: tablet (680px) shows drawer inline +- State: `HomeState {loading, finish, error, empty, offline}` + +### LoginScaffold +- Gradient `DecoratedBox` background (primary→primaryContainer / surface→surfaceContainerLowest) +- Form card with 24dp radius, shadow +- Logo: text or image mode +- Responsive: portrait=column, landscape=row + +### CourseScaffold (~1700 lines) +- `AppBar` + `SemesterPicker` + settings gear +- List tab + Table tab (course color-coded) +- Screenshot capture with `RepaintBoundary` +- `CourseConfig` InheritedWidget for display settings +- 12 predefined course colors +- Complex state: `CourseState {loading, finish, error, empty, offlineEmpty, custom}` + +### ScoreScaffold (~925 lines) +- `AppBar` + `SemesterPicker` +- Score list + Analysis view toggle +- Score color by range (90+ green, 80+ light green, 70+ blue, 60+ orange, <60 red) +- Analysis: ScorePRCard, ScoreStatisticsCard, ScoreDistributionCard, ScoreCreditSummaryCard + +### UserInfoScaffold +- `SliverAppBar` (expandedHeight 200, gradient FlexibleSpaceBar) +- Avatar with Hero animation +- QR code / Code39 barcode toggle +- `CustomScrollView` with pinned app bar + +### ImageViewerScaffold +- `AppBar` + optional actions +- `TabBar` (if multiple images) +- `TabBarView` with `PhotoView` galleries + +## Performance Notes + +- `GlassQuality.standard` — default, works on all platforms +- `GlassQuality.premium` — Impeller only, for static surfaces +- `LiquidGlassWidgets.initialize()` pre-warms shaders (call in `main()`) +- Library respects system Reduce Motion / Reduce Transparency (WCAG) + +## Code Style Reminders + +- `always_specify_types: true` +- `prefer_single_quotes: true` +- 80 char line limit +- Constructors first in class +- Conventional Commits: `feat(liquid_glass):`, `fix(liquid_glass):`, etc. diff --git a/packages/ap_common_liquid_glass/lib/ap_common_liquid_glass.dart b/packages/ap_common_liquid_glass/lib/ap_common_liquid_glass.dart new file mode 100644 index 00000000..b6e4edf6 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/ap_common_liquid_glass.dart @@ -0,0 +1,50 @@ +/// Liquid Glass design language for AP series apps. +/// +/// This package provides glass-effect scaffold variants and widgets +/// powered by [liquid_glass_widgets]. It is an opt-in extension of +/// [ap_common_flutter_ui] — existing apps work unchanged. +/// +/// ## Quick Start +/// +/// 1. Add `ap_common_liquid_glass` to your dependencies. +/// 2. Initialize shaders in `main()`: +/// ```dart +/// await LiquidGlassWidgets.initialize(); +/// ``` +/// 3. Use [LiquidGlassApApp] instead of [ApApp]: +/// ```dart +/// LiquidGlassApApp(home: MyHomePage()) +/// ``` +/// 4. Use glass scaffold variants: +/// ```dart +/// GlassHomePageScaffold(...) +/// GlassLoginScaffold(...) +/// ``` +library ap_common_liquid_glass; + +export 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +export 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +export 'src/glass_theme_bridge.dart'; +export 'src/liquid_glass_app.dart'; +export 'src/pages/glass_about_us_page.dart'; +export 'src/pages/glass_announcement_content_page.dart'; +export 'src/pages/glass_ap_course_page.dart'; +export 'src/pages/glass_ap_login_page.dart'; +export 'src/pages/glass_ap_score_page.dart'; +export 'src/scaffold/glass_course_scaffold.dart'; +export 'src/scaffold/glass_home_page_scaffold.dart'; +export 'src/scaffold/glass_image_viewer_scaffold.dart'; +export 'src/scaffold/glass_login_scaffold.dart'; +export 'src/scaffold/glass_score_scaffold.dart'; +export 'src/scaffold/glass_user_info_scaffold.dart'; +export 'src/widgets/glass_ap_drawer.dart'; +export 'src/widgets/glass_course_content.dart'; +export 'src/widgets/glass_course_list.dart'; +export 'src/widgets/glass_course_table_view.dart'; +export 'src/widgets/glass_dialog.dart'; +export 'src/widgets/glass_floating_toolbar.dart'; +export 'src/widgets/glass_score_analysis_cards.dart'; +export 'src/widgets/glass_score_analysis_tab.dart'; +export 'src/widgets/glass_score_list_tab.dart'; +export 'src/widgets/glass_semester_picker.dart'; diff --git a/packages/ap_common_liquid_glass/lib/src/glass_theme_bridge.dart b/packages/ap_common_liquid_glass/lib/src/glass_theme_bridge.dart new file mode 100644 index 00000000..b04962b4 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/glass_theme_bridge.dart @@ -0,0 +1,120 @@ +import 'dart:math' as math; + +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// Apple standard light angle: 135° (upper-left light source). +const double _kLightAngle = 0.75 * math.pi; + +/// Bridges [ApTheme] seed colors to [GlassThemeData]. +/// +/// Maps the current [ColorScheme] to [GlassGlowColors] and +/// configures [LiquidGlassSettings] following Apple's Liquid +/// Glass design guidelines (consistent light angle, appropriate +/// glass tint per brightness, etc.). +/// +/// ```dart +/// GlassTheme( +/// data: GlassThemeBridge.fromContext(context), +/// child: child, +/// ) +/// ``` +class GlassThemeBridge { + const GlassThemeBridge._(); + + /// Creates a [GlassThemeData] aligned with the current + /// [ApTheme] / [ColorScheme]. + /// + /// Uses [GlassQuality.standard] for broad compatibility. + /// Override via [LiquidGlassSettings] for per-widget control. + static GlassThemeData fromContext( + BuildContext context, + ) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return GlassThemeData( + light: GlassThemeVariant( + quality: GlassQuality.standard, + settings: const LiquidGlassSettings( + glassColor: Color.fromRGBO( + 255, + 255, + 255, + 0.12, + ), + thickness: 30, + blur: 3, + lightAngle: _kLightAngle, + lightIntensity: 2.0, + refractiveIndex: 0.7, + ), + glowColors: GlassGlowColors( + primary: colorScheme.primary, + secondary: colorScheme.secondary, + success: const Color(0xFF34C759), + warning: const Color(0xFFFF9500), + danger: colorScheme.error, + info: const Color(0xFF5AC8FA), + ), + ), + dark: GlassThemeVariant( + quality: GlassQuality.standard, + settings: const LiquidGlassSettings( + glassColor: Color.fromRGBO( + 255, + 255, + 255, + 0.08, + ), + thickness: 40, + lightAngle: _kLightAngle, + lightIntensity: 1.5, + refractiveIndex: 0.7, + ), + glowColors: GlassGlowColors( + primary: colorScheme.primary, + secondary: colorScheme.secondary, + success: const Color(0xFF30D158), + warning: const Color(0xFFFF9F0A), + danger: colorScheme.error, + info: const Color(0xFF64D2FF), + ), + ), + ); + } + + /// Creates a [GlassThemeData] with custom settings while + /// still bridging [GlassGlowColors] from the [ColorScheme]. + static GlassThemeData withSettings({ + required BuildContext context, + LiquidGlassSettings? lightSettings, + LiquidGlassSettings? darkSettings, + GlassQuality quality = GlassQuality.standard, + }) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final GlassGlowColors glowColors = GlassGlowColors( + primary: colorScheme.primary, + secondary: colorScheme.secondary, + success: const Color(0xFF34C759), + warning: const Color(0xFFFF9500), + danger: colorScheme.error, + info: const Color(0xFF5AC8FA), + ); + + return GlassThemeData( + light: GlassThemeVariant( + settings: lightSettings, + quality: quality, + glowColors: glowColors, + ), + dark: GlassThemeVariant( + settings: darkSettings, + quality: quality, + glowColors: glowColors, + ), + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/liquid_glass_app.dart b/packages/ap_common_liquid_glass/lib/src/liquid_glass_app.dart new file mode 100644 index 00000000..c9327b76 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/liquid_glass_app.dart @@ -0,0 +1,281 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/glass_theme_bridge.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A convenience wrapper around [MaterialApp] that applies both +/// [ApTheme] and [GlassTheme] setup. +/// +/// This is the glass-enhanced equivalent of [ApApp]. It: +/// - Applies [GlassTheme] bridged from [ApTheme] seed colors +/// - Manages theme mode, locale, and color preferences +/// +/// **Important:** +/// - Call [LiquidGlassWidgets.initialize] in `main()` and wrap +/// with [LiquidGlassWidgets.wrap] at the `runApp` level. +/// - Do NOT wrap with [TranslationProvider] externally — this +/// widget already includes one. Nesting two providers of the +/// same locale type causes a duplicate GlobalKey crash. +/// +/// ```dart +/// void main() async { +/// WidgetsFlutterBinding.ensureInitialized(); +/// await LiquidGlassWidgets.initialize(); +/// runApp(LiquidGlassWidgets.wrap(const MyApp())); +/// } +/// +/// class MyApp extends StatelessWidget { +/// const MyApp({super.key}); +/// +/// @override +/// Widget build(BuildContext context) { +/// // No TranslationProvider needed here. +/// return LiquidGlassApApp( +/// home: const GlassHomePage(), +/// ); +/// } +/// } +/// ``` +class LiquidGlassApApp extends StatefulWidget { + const LiquidGlassApApp({ + super.key, + this.home, + this.routes = const {}, + this.onGenerateTitle, + this.additionalLocalizationsDelegates = + const >[], + this.initialThemeMode = ThemeMode.system, + this.themeModePreferenceKey = 'pref_theme_mode_index', + this.languageCodePreferenceKey = 'pref_language_code', + this.debugShowCheckedModeBanner = false, + this.navigatorObservers = const [], + this.onGenerateRoute, + this.quality = GlassQuality.standard, + this.glassSettings, + }); + + /// The default route widget. + final Widget? home; + + /// Named routes. + final Map routes; + + /// Callback to generate the app title. + final GenerateAppTitle? onGenerateTitle; + + /// Extra localization delegates. + final List> + additionalLocalizationsDelegates; + + /// The initial theme mode. + final ThemeMode initialThemeMode; + + /// Preference key for storing theme mode index. + final String themeModePreferenceKey; + + /// Preference key for storing language code. + final String languageCodePreferenceKey; + + /// Whether to show the debug banner. + final bool debugShowCheckedModeBanner; + + /// Navigator observers. + final List navigatorObservers; + + /// Custom route generation. + final RouteFactory? onGenerateRoute; + + /// Glass rendering quality. + final GlassQuality quality; + + /// Optional glass effect settings (thickness, blur, specular). + final LiquidGlassSettings? glassSettings; + + /// Find the nearest [LiquidGlassApAppState] in the widget tree. + static LiquidGlassApAppState of(BuildContext context) { + final _LiquidGlassApAppScope? scope = context + .dependOnInheritedWidgetOfExactType<_LiquidGlassApAppScope>(); + assert(scope != null, 'No LiquidGlassApApp found in context'); + return scope!.state; + } + + @override + State createState() => LiquidGlassApAppState(); +} + +class LiquidGlassApAppState extends State + with WidgetsBindingObserver { + late ThemeMode _themeMode; + int _currentColorIndex = 0; + Color? _customColor; + + ThemeMode get themeMode => _themeMode; + + @override + void initState() { + super.initState(); + final int themeIndex = PreferenceUtil.instance.getInt( + widget.themeModePreferenceKey, + 0, + ); + _themeMode = + themeIndex >= 0 && themeIndex < ThemeMode.values.length + ? ThemeMode.values[themeIndex] + : ThemeMode.system; + _currentColorIndex = PreferenceUtil.instance.getInt( + ApTheme.PREF_COLOR_INDEX, + 0, + ); + final int customColorValue = PreferenceUtil.instance.getInt( + ApTheme.PREF_CUSTOM_COLOR, + 0, + ); + if (_currentColorIndex == ApTheme.customColorIndex && + customColorValue != 0) { + _customColor = Color(customColorValue); + } + WidgetsBinding.instance.addObserver(this); + _initLocale(); + Future.microtask(() { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + systemNavigationBarContrastEnforced: true, + systemNavigationBarColor: Colors.transparent, + ), + ); + }); + } + + Future _initLocale() async { + final String languageCode = PreferenceUtil.instance.getString( + widget.languageCodePreferenceKey, + ApSupportLanguageConstants.system, + ); + if (languageCode == ApSupportLanguageConstants.system) { + await useApDeviceLocale(); + } else { + final Locale locale = Locale( + languageCode, + languageCode == ApSupportLanguageConstants.zh ? 'TW' : null, + ); + await setApLocaleFromFlutter(locale); + } + if (mounted) setState(() {}); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangePlatformBrightness() { + setState(() {}); + super.didChangePlatformBrightness(); + } + + /// Change the app's theme mode. + void setThemeMode(ThemeMode mode) { + setState(() => _themeMode = mode); + } + + /// Change the app's locale. + void setLocale(Locale locale) { + setApLocaleFromFlutter(locale); + setState(() {}); + } + + /// Change the app's theme color. + void setThemeColor(int index, Color? custom) { + setState(() { + _currentColorIndex = index; + _customColor = custom; + }); + } + + /// Force a rebuild. + void update() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return _LiquidGlassApAppScope( + state: this, + child: ApTheme( + themeMode: _themeMode, + currentColorIndex: _currentColorIndex, + customColor: _customColor, + preferences: PreferenceUtil.instance, + child: Builder( + builder: (BuildContext context) { + final Color seedColor = + ApTheme.of(context).seedColor; + final GlassThemeData glassThemeData = + GlassThemeBridge.fromContext(context); + return TranslationProvider( + child: Builder( + builder: (BuildContext context) { + return GlassTheme( + data: glassThemeData, + child: MaterialApp( + home: widget.home, + onGenerateTitle: + widget.onGenerateTitle, + debugShowCheckedModeBanner: widget + .debugShowCheckedModeBanner, + routes: widget.routes, + onGenerateRoute: + widget.onGenerateRoute, + theme: ApTheme.light(seedColor), + darkTheme: + ApTheme.dark(seedColor), + themeMode: _themeMode, + locale: TranslationProvider.of( + context, + ).flutterLocale, + navigatorObservers: + widget.navigatorObservers, + supportedLocales: + AppLocaleUtils + .supportedLocales, + localizationsDelegates: + >[ + ...widget + .additionalLocalizationsDelegates, + GlobalMaterialLocalizations + .delegate, + GlobalWidgetsLocalizations + .delegate, + GlobalCupertinoLocalizations + .delegate, + ], + ), + ); + }, + ), + ); + }, + ), + ), + ); + } +} + +class _LiquidGlassApAppScope extends InheritedWidget { + const _LiquidGlassApAppScope({ + required this.state, + required super.child, + }); + + final LiquidGlassApAppState state; + + @override + bool updateShouldNotify(_LiquidGlassApAppScope oldWidget) => true; +} diff --git a/packages/ap_common_liquid_glass/lib/src/pages/glass_about_us_page.dart b/packages/ap_common_liquid_glass/lib/src/pages/glass_about_us_page.dart new file mode 100644 index 00000000..a151337a --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/pages/glass_about_us_page.dart @@ -0,0 +1,398 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_floating_toolbar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [AboutUsPage]. +/// +/// Replaces the [SliverAppBar] with [GlassFloatingToolbar], +/// [Card] surfaces with [GlassCard], and uses +/// [AdaptiveLiquidGlassLayer] for the glass effect. +class GlassAboutUsPage extends StatefulWidget { + const GlassAboutUsPage({ + super.key, + required this.assetImage, + required this.fbFanPageUrl, + required this.fbFanPageId, + this.instagramUsername, + required this.githubUrl, + required this.githubName, + required this.email, + required this.appLicense, + this.actions, + this.applicationName, + this.applicationVersion, + this.applicationIcon, + this.applicationLegalese, + }); + + final String assetImage; + final String fbFanPageUrl; + final String fbFanPageId; + final String? instagramUsername; + final String githubUrl; + final String githubName; + final String email; + final String appLicense; + final String? applicationName; + final String? applicationVersion; + final Widget? applicationIcon; + final String? applicationLegalese; + final List? actions; + + @override + GlassAboutUsPageState createState() => + GlassAboutUsPageState(); +} + +class GlassAboutUsPageState + extends State { + @override + void initState() { + AnalyticsUtil.instance.setCurrentScreen( + 'GlassAboutUsPage', + 'glass_about_us_page.dart', + ); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final ApLocalizations ap = context.ap; + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final double topPadding = + MediaQuery.of(context).padding.top; + + return AdaptiveLiquidGlassLayer( + child: Scaffold( + body: Stack( + children: [ + ListView( + padding: EdgeInsets.only( + top: topPadding + 52, + bottom: 32, + ), + children: [ + _buildHeader(context), + const SizedBox(height: 16), + _buildInfoCard( + ap.aboutAuthorTitle, + ap.aboutAuthorContent, + ), + _buildInfoCard( + ap.about, + ap.aboutUsContent, + ), + _buildInfoCard( + ap.aboutRecruitTitle, + ap.aboutRecruitContent, + ), + _buildInfoCardWithLogo( + ap.aboutItcTitle, + ap.aboutItcContent, + ApImageAssets.nkutstItc, + ), + _buildInfoCardWithLogo( + ap.aboutNsysuCodeClubTitle, + ap.aboutNsysuCodeClubContent, + ApImageAssets.nsysuGdsc, + ), + _buildContactCard( + context, + ap, + colorScheme, + ), + _buildInfoCard( + ap.aboutOpenSourceTitle, + widget.appLicense, + ), + ], + ), + GlassFloatingToolbar( + leading: [ + GestureDetector( + onTap: () => Navigator.pop(context), + child: const Icon( + Icons.arrow_back_ios_new_rounded, + size: 18, + ), + ), + const SizedBox(width: 8), + Text( + ap.about, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ], + trailing: [ + ...widget.actions ?? [], + IconButton( + icon: Icon(ApIcon.codeIcon), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => LicensePage( + applicationName: + widget.applicationName, + applicationVersion: + widget + .applicationVersion, + applicationIcon: + widget.applicationIcon, + applicationLegalese: + widget + .applicationLegalese, + ), + ), + ); + AnalyticsUtil.instance + .logEvent('license_page_click'); + }, + iconSize: 22, + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 36, + minHeight: 36, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildHeader(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image.asset( + widget.assetImage, + height: 180, + width: double.infinity, + fit: BoxFit.cover, + ), + ), + ); + } + + Widget _buildInfoCard(String title, String content) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 6, + horizontal: 16, + ), + child: GlassCard( + useOwnLayer: true, + child: SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SelectableLinkify( + text: title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + SelectableLinkify( + text: content, + style: TextStyle( + fontSize: 14, + color: + colorScheme.onSurfaceVariant, + ), + options: const LinkifyOptions( + humanize: false, + ), + onOpen: (LinkableElement link) => + PlatformUtil.instance + .launchUrl(link.url), + ), + ], + ), + ), + ), + ); + } + + Widget _buildInfoCardWithLogo( + String title, + String content, + String logoAsset, + ) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 6, + horizontal: 16, + ), + child: GlassCard( + useOwnLayer: true, + child: SizedBox( + width: double.infinity, + child: Stack( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only( + right: 72, + ), + child: SelectableLinkify( + text: title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(height: 8), + SelectableLinkify( + text: content, + style: TextStyle( + fontSize: 14, + color: colorScheme + .onSurfaceVariant, + ), + options: const LinkifyOptions( + humanize: false, + ), + onOpen: (LinkableElement link) => + PlatformUtil.instance + .launchUrl(link.url), + ), + ], + ), + Positioned( + top: 0, + right: 0, + child: Image.asset( + logoAsset, + width: 56, + fit: BoxFit.cover, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildContactCard( + BuildContext context, + ApLocalizations ap, + ColorScheme colorScheme, + ) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 6, + horizontal: 16, + ), + child: GlassCard( + useOwnLayer: true, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + ap.aboutContactUsTitle, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + _buildContactIcon( + ApImageAssets.fb, + colorScheme.onSurface, + () { + PlatformUtil.instance.launchUrl( + 'https://m.me/' + '${widget.fbFanPageId}', + ); + AnalyticsUtil.instance + .logEvent('fb_click'); + }, + ), + if (widget.instagramUsername + case final String username?) + _buildContactIcon( + ApImageAssets.instagram, + null, + () { + PlatformUtil.instance.launchUrl( + 'https://ig.me/m/$username', + ); + AnalyticsUtil.instance + .logEvent('instagram_click'); + }, + ), + _buildContactIcon( + ApImageAssets.github, + colorScheme.onSurface, + () { + PlatformUtil.instance + .launchUrl(widget.githubUrl); + AnalyticsUtil.instance + .logEvent('github_click'); + }, + ), + _buildContactIcon( + ApImageAssets.email, + colorScheme.onSurface, + () { + PlatformUtil.instance.launchUrl( + 'mailto:${widget.email}', + ); + AnalyticsUtil.instance + .logEvent('email_click'); + }, + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildContactIcon( + String asset, + Color? color, + VoidCallback onTap, + ) { + return GlassIconButton( + icon: Image.asset( + asset, + color: color, + width: 28, + height: 28, + ), + onPressed: onTap, + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/pages/glass_announcement_content_page.dart b/packages/ap_common_liquid_glass/lib/src/pages/glass_announcement_content_page.dart new file mode 100644 index 00000000..db8cc438 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/pages/glass_announcement_content_page.dart @@ -0,0 +1,289 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_floating_toolbar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; +import 'package:photo_view/photo_view.dart'; + +/// A glass-enhanced version of [AnnouncementContentPage]. +/// +/// Uses [GlassFloatingToolbar] for navigation, +/// [GlassCard] for content sections, and +/// [GlassButton] for links. +class GlassAnnouncementContentPage extends StatefulWidget { + const GlassAnnouncementContentPage({ + super.key, + required this.announcement, + }); + + final Announcement announcement; + + @override + GlassAnnouncementContentPageState createState() => + GlassAnnouncementContentPageState(); +} + +class GlassAnnouncementContentPageState + extends State { + @override + void initState() { + AnalyticsUtil.instance.setCurrentScreen( + 'GlassAnnouncementContentPage', + 'glass_announcement_content_page.dart', + ); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final double topPadding = + MediaQuery.of(context).padding.top; + + return AdaptiveLiquidGlassLayer( + child: Scaffold( + body: Stack( + children: [ + OrientationBuilder( + builder: ( + _, + Orientation orientation, + ) { + return orientation == + Orientation.portrait + ? SingleChildScrollView( + padding: EdgeInsets.only( + top: topPadding + 52, + bottom: 32, + ), + child: Column( + children: + _renderContent( + orientation, + ), + ), + ) + : Padding( + padding: EdgeInsets.only( + top: topPadding + 52, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment + .center, + children: + _renderContent( + orientation, + ), + ), + ); + }, + ), + GlassFloatingToolbar( + leading: [ + GestureDetector( + onTap: () => + Navigator.pop(context), + child: const Icon( + Icons + .arrow_back_ios_new_rounded, + size: 18, + ), + ), + const SizedBox(width: 8), + Flexible( + child: Text( + context.ap.announcements, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + overflow: + TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ); + } + + List _renderContent( + Orientation orientation, + ) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + final Widget image = GestureDetector( + onTap: () { + ApUtils.pushCupertinoStyle( + context, + Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + title: Text( + widget.announcement.title, + ), + ), + body: PhotoView( + imageProvider: + (ApUtils.isSupportCacheNetworkImage + ? CachedNetworkImageProvider( + widget.announcement + .imgUrl, + ) + : NetworkImage( + widget.announcement + .imgUrl, + )) + as ImageProvider?, + ), + ), + ); + AnalyticsUtil.instance.logEvent( + 'announcement_content_image_click', + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: ClipRRect( + borderRadius: + BorderRadius.circular(16), + child: AspectRatio( + aspectRatio: + orientation == Orientation.portrait + ? 4 / 3 + : 9 / 16, + child: Hero( + tag: + widget.announcement.hashCode, + child: ApNetworkImage( + url: + widget.announcement.imgUrl, + ), + ), + ), + ), + ), + ); + + final Widget contentCard = Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: GlassCard( + useOwnLayer: true, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Hero( + tag: + ApConstants.tagAnnouncementTitle, + child: Material( + color: Colors.transparent, + child: SelectableText( + widget.announcement.title, + style: TextStyle( + fontSize: 20, + color: colorScheme.onSurface, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + if (widget + .announcement + .publishedTime != + null) ...[ + const SizedBox(height: 8), + Text( + widget.announcement + .publishedTime!, + style: TextStyle( + fontSize: 13, + color: colorScheme + .onSurfaceVariant, + ), + ), + ], + const SizedBox(height: 12), + SelectableLinkify( + text: widget + .announcement.description, + style: TextStyle( + fontSize: 15, + color: + colorScheme.onSurfaceVariant, + height: 1.5, + ), + linkStyle: TextStyle( + color: colorScheme.primary, + ), + options: const LinkifyOptions( + humanize: false, + ), + onOpen: (LinkableElement link) => + PlatformUtil.instance + .launchUrl(link.url), + ), + if (widget.announcement.url != + null && + widget.announcement.url! + .isNotEmpty) ...[ + const SizedBox(height: 16), + Center( + child: GlassButton.custom( + onTap: () { + PlatformUtil.instance + .launchUrl( + widget.announcement.url!, + ); + AnalyticsUtil.instance + .logEvent( + 'announcement_link_click', + ); + }, + width: 56, + height: 56, + child: Icon( + ApIcon.exitToApp, + color: + colorScheme.onSurface, + ), + ), + ), + ], + ], + ), + ), + ); + + if (orientation == Orientation.portrait) { + return [ + const SizedBox(height: 8), + image, + const SizedBox(height: 16), + contentCard, + ]; + } else { + return [ + const SizedBox(width: 16), + Expanded(child: image), + const SizedBox(width: 16), + Expanded( + child: SingleChildScrollView( + child: contentCard, + ), + ), + const SizedBox(width: 16), + ]; + } + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_course_page.dart b/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_course_page.dart new file mode 100644 index 00000000..a12cb0f8 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_course_page.dart @@ -0,0 +1,162 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/scaffold/glass_course_scaffold.dart'; +import 'package:flutter/material.dart'; + +/// A ready-to-use glass course page that handles semester +/// loading and state management internally. +/// +/// This is the glass-enhanced equivalent of [ApCoursePage]. +class GlassApCoursePage extends StatefulWidget { + const GlassApCoursePage({ + super.key, + required this.onLoadSemesters, + required this.onLoadCourse, + this.title, + this.enableNotifyControl = true, + this.enableAddToCalendar = true, + this.enableCaptureCourseTable = false, + this.androidResourceIcon, + this.actions, + this.showSectionTime, + this.showInstructors, + this.showClassroomLocation, + this.courseNotifySaveKey, + }); + + final Future Function() + onLoadSemesters; + final Future Function( + Semester semester, + ) onLoadCourse; + final String? title; + final bool enableNotifyControl; + final bool enableAddToCalendar; + final bool enableCaptureCourseTable; + final String? androidResourceIcon; + final List? actions; + final bool? showSectionTime; + final bool? showInstructors; + final bool? showClassroomLocation; + final String? courseNotifySaveKey; + + @override + State createState() => + _GlassApCoursePageState(); +} + +class _GlassApCoursePageState + extends State { + DataState _state = + const DataLoading(); + SemesterData? _semesterData; + CourseNotifyData? _notifyData; + + String get _notifyCacheKey => + widget.courseNotifySaveKey ?? + PreferenceUtil.instance.getString( + ApConstants.currentSemesterCode, + ApConstants.semesterLatest, + ); + + @override + void initState() { + super.initState(); + _loadSemesters(); + } + + Future _loadSemesters() async { + try { + _semesterData = await widget.onLoadSemesters(); + _loadCourse(); + } catch (_) { + if (mounted) { + setState( + () => _state = + const DataError(), + ); + } + } + } + + Future _loadCourse() async { + if (_semesterData == null) return; + setState( + () => _state = const DataLoading(), + ); + try { + final Semester semester = _semesterData! + .data[_semesterData!.currentIndex]; + final CourseData courseData = + await widget.onLoadCourse(semester); + if (mounted) { + setState(() { + if (courseData.courses.isEmpty) { + _state = const DataEmpty(); + } else { + _state = DataLoaded( + courseData, + ); + _notifyData = CourseNotifyData.load( + _notifyCacheKey, + ); + } + }); + } + } catch (e) { + if (mounted) { + setState( + () => _state = DataError( + hint: e.toString(), + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return GlassCourseScaffold( + state: _state.when( + loading: () => CourseState.loading, + loaded: (_, __) => CourseState.finish, + error: (_) => CourseState.error, + empty: (_) => CourseState.empty, + ), + courseData: + _state.dataOrNull ?? CourseData.empty(), + notifyData: _notifyData, + title: widget.title, + customHint: _state is DataLoaded + ? (_state as DataLoaded).hint + : null, + customStateHint: + _state is DataError + ? (_state as DataError).hint + : null, + courseNotifySaveKey: _notifyCacheKey, + semesterData: _semesterData, + enableNotifyControl: + widget.enableNotifyControl, + enableAddToCalendar: + widget.enableAddToCalendar, + enableCaptureCourseTable: + widget.enableCaptureCourseTable, + androidResourceIcon: + widget.androidResourceIcon, + actions: widget.actions, + showSectionTime: widget.showSectionTime, + showInstructors: widget.showInstructors, + showClassroomLocation: + widget.showClassroomLocation, + onSelect: (int index) { + _semesterData = _semesterData! + .copyWith(currentIndex: index); + _loadCourse(); + }, + onRefresh: () async { + await _loadCourse(); + return null; + }, + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_login_page.dart b/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_login_page.dart new file mode 100644 index 00000000..01a68dd5 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_login_page.dart @@ -0,0 +1,290 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/scaffold/glass_login_scaffold.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// A ready-to-use glass login page with built-in preference +/// handling for remember password and auto-login. +/// +/// This is the glass-enhanced equivalent of [ApLoginPage]. +class GlassApLoginPage extends StatefulWidget { + const GlassApLoginPage({ + super.key, + required this.logoSource, + required this.onLogin, + this.logoMode = LogoMode.text, + this.enableOfflineLogin = false, + this.onOfflineLogin, + this.extraForms, + this.usernamePreferenceKey = 'pref_username', + this.passwordPreferenceKey = 'pref_password', + this.rememberPasswordPreferenceKey = + 'pref_remember_password', + this.autoLoginPreferenceKey = 'pref_auto_login', + }); + + final LogoMode logoMode; + final String logoSource; + final Future Function( + String username, + String password, + ) onLogin; + final bool enableOfflineLogin; + final Future Function( + String username, + String password, + )? onOfflineLogin; + final List? extraForms; + final String usernamePreferenceKey; + final String passwordPreferenceKey; + final String rememberPasswordPreferenceKey; + final String autoLoginPreferenceKey; + + @override + State createState() => + _GlassApLoginPageState(); +} + +class _GlassApLoginPageState + extends State { + ApLocalizations get ap => context.ap; + + final TextEditingController _username = + TextEditingController(); + final TextEditingController _password = + TextEditingController(); + final FocusNode _usernameFocus = FocusNode(); + final FocusNode _passwordFocus = FocusNode(); + + bool _isRememberPassword = true; + bool _isAutoLogin = false; + + @override + void initState() { + super.initState(); + _loadPreferences(); + } + + @override + void dispose() { + _username.dispose(); + _password.dispose(); + _usernameFocus.dispose(); + _passwordFocus.dispose(); + super.dispose(); + } + + void _loadPreferences() { + _isRememberPassword = PreferenceUtil.instance + .getBool( + widget.rememberPasswordPreferenceKey, + true, + ); + _isAutoLogin = PreferenceUtil.instance.getBool( + widget.autoLoginPreferenceKey, + false, + ); + setState(() { + _username.text = PreferenceUtil.instance + .getString( + widget.usernamePreferenceKey, + '', + ); + _password.text = _isRememberPassword + ? PreferenceUtil.instance.getStringSecurity( + widget.passwordPreferenceKey, + '', + ) + : ''; + }); + } + + @override + Widget build(BuildContext context) { + return GlassLoginScaffold( + logoMode: widget.logoMode, + logoSource: widget.logoSource, + forms: [ + ApTextField( + controller: _username, + keyboardType: TextInputType.emailAddress, + focusNode: _usernameFocus, + nextFocusNode: _passwordFocus, + labelText: ap.studentId, + autofillHints: const [ + AutofillHints.username, + ], + ), + ApTextField( + obscureText: true, + textInputAction: TextInputAction.send, + controller: _password, + focusNode: _passwordFocus, + onSubmitted: (_) { + _passwordFocus.unfocus(); + _login(); + }, + labelText: ap.password, + autofillHints: const [ + AutofillHints.password, + ], + ), + const SizedBox(height: 8.0), + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + TextCheckBox( + text: ap.autoLogin, + value: _isAutoLogin, + onChanged: (bool? value) { + setState(() { + _isAutoLogin = value ?? false; + _isRememberPassword = _isAutoLogin; + _saveLoginPreferences(); + }); + }, + ), + TextCheckBox( + text: ap.rememberPassword, + value: _isRememberPassword, + onChanged: (bool? value) { + setState(() { + _isRememberPassword = + value ?? false; + if (!_isRememberPassword) { + _isAutoLogin = false; + } + _saveLoginPreferences(); + }); + }, + ), + ], + ), + if (widget.extraForms != null) + ...widget.extraForms!, + const SizedBox(height: 8.0), + ApButton( + text: ap.login, + onPressed: _login, + ), + if (widget.enableOfflineLogin) + ApFlatButton( + text: ap.offlineLogin, + onPressed: _offlineLogin, + ), + ], + ); + } + + void _saveLoginPreferences() { + PreferenceUtil.instance.setBool( + widget.autoLoginPreferenceKey, + _isAutoLogin, + ); + PreferenceUtil.instance.setBool( + widget.rememberPasswordPreferenceKey, + _isRememberPassword, + ); + } + + Future _login() async { + if (_username.text.isEmpty || + _password.text.isEmpty) { + UiUtil.instance.showToast( + context, + ap.doNotEmpty, + ); + return; + } + + showDialog( + context: context, + builder: (BuildContext context) => PopScope( + canPop: false, + child: ProgressDialog(ap.logining), + ), + barrierDismissible: false, + ); + + try { + final bool success = await widget.onLogin( + _username.text, + _password.text, + ); + if (!mounted) return; + Navigator.of(context, rootNavigator: true).pop(); + + if (success) { + _saveCredentials(); + TextInput.finishAutofillContext(); + Navigator.of(context).pop(true); + } + } catch (e) { + if (!mounted) return; + Navigator.of(context, rootNavigator: true).pop(); + UiUtil.instance + .showToast(context, e.toString()); + } + } + + void _saveCredentials() { + PreferenceUtil.instance.setString( + widget.usernamePreferenceKey, + _username.text, + ); + if (_isRememberPassword) { + PreferenceUtil.instance.setStringSecurity( + widget.passwordPreferenceKey, + _password.text, + ); + } + } + + Future _offlineLogin() async { + if (widget.onOfflineLogin != null) { + final bool success = + await widget.onOfflineLogin!( + _username.text, + _password.text, + ); + if (success && mounted) { + Navigator.of(context).pop(true); + } + return; + } + + final String savedUsername = + PreferenceUtil.instance.getString( + widget.usernamePreferenceKey, + '', + ); + final String savedPassword = + PreferenceUtil.instance.getStringSecurity( + widget.passwordPreferenceKey, + '', + ); + + if (savedUsername.isEmpty || + savedPassword.isEmpty) { + UiUtil.instance.showToast( + context, + ap.noOfflineLoginData, + ); + } else if (savedUsername != _username.text || + savedPassword != _password.text) { + UiUtil.instance.showToast( + context, + ap.offlineLoginPasswordError, + ); + } else { + UiUtil.instance.showToast( + context, + ap.loadOfflineData, + ); + if (mounted) { + Navigator.of(context).pop(true); + } + } + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_score_page.dart b/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_score_page.dart new file mode 100644 index 00000000..f299157a --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_score_page.dart @@ -0,0 +1,129 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/scaffold/glass_score_scaffold.dart'; +import 'package:flutter/material.dart'; + +/// A ready-to-use glass score page that handles semester +/// loading and state management internally. +/// +/// This is the glass-enhanced equivalent of [ApScorePage]. +class GlassApScorePage extends StatefulWidget { + const GlassApScorePage({ + super.key, + required this.onLoadSemesters, + required this.onLoadScore, + this.title, + this.middleTitle, + this.finalTitle, + this.middleScoreBuilder, + this.finalScoreBuilder, + this.detailBuilder, + this.isShowSearchButton = false, + }); + + final Future Function() + onLoadSemesters; + final Future Function( + Semester semester, + ) onLoadScore; + final String? title; + final String? middleTitle; + final String? finalTitle; + final Widget Function(int index)? + middleScoreBuilder; + final Widget Function(int index)? + finalScoreBuilder; + final List Function(ScoreData?)? + detailBuilder; + final bool isShowSearchButton; + + @override + State createState() => + _GlassApScorePageState(); +} + +class _GlassApScorePageState + extends State { + DataState _state = + const DataLoading(); + SemesterData? _semesterData; + + @override + void initState() { + super.initState(); + _loadSemesters(); + } + + Future _loadSemesters() async { + try { + _semesterData = await widget.onLoadSemesters(); + _loadScore(); + } catch (_) { + if (mounted) { + setState( + () => _state = + const DataError(), + ); + } + } + } + + Future _loadScore() async { + if (_semesterData == null) return; + setState( + () => _state = + const DataLoading(), + ); + try { + final Semester semester = _semesterData! + .data[_semesterData!.currentIndex]; + final ScoreData? scoreData = + await widget.onLoadScore(semester); + if (mounted) { + setState(() { + if (scoreData == null || + scoreData.scores.isEmpty) { + _state = const DataEmpty(); + } else { + _state = + DataLoaded(scoreData); + } + }); + } + } catch (e) { + if (mounted) { + setState( + () => _state = DataError( + hint: e.toString(), + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return GlassScoreScaffold( + state: _state.when( + loading: () => ScoreState.loading, + loaded: (_, __) => ScoreState.finish, + error: (_) => ScoreState.error, + empty: (_) => ScoreState.empty, + ), + scoreData: _state.dataOrNull, + onRefresh: _loadScore, + title: widget.title, + semesterData: _semesterData, + middleTitle: widget.middleTitle, + finalTitle: widget.finalTitle, + middleScoreBuilder: widget.middleScoreBuilder, + finalScoreBuilder: widget.finalScoreBuilder, + isShowSearchButton: + widget.isShowSearchButton, + onSelect: (int index) { + _semesterData = _semesterData! + .copyWith(currentIndex: index); + _loadScore(); + }, + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_course_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_course_scaffold.dart new file mode 100644 index 00000000..25dae235 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_course_scaffold.dart @@ -0,0 +1,639 @@ +import 'dart:ui' as ui; + +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_course_content.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_course_list.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_course_table_view.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_dialog.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_floating_toolbar.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_semester_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +const String _kCourseInvisibleKey = + '${ApConstants.packageName}.course_invisible_'; + +/// A glass-enhanced version of [CourseScaffold]. +/// +/// Replaces the Material [AppBar] with [GlassAppBar] and the +/// [FloatingActionButton]s with [GlassButton] widgets while +/// reusing the body content widgets ([CourseTableView], +/// [CourseList], etc.) from the original scaffold. +class GlassCourseScaffold extends StatefulWidget { + const GlassCourseScaffold({ + super.key, + required this.state, + required this.courseData, + this.title, + this.customHint, + this.semesterData, + this.onSelect, + this.onRefresh, + this.actions, + this.enableNotifyControl = true, + this.notifyData, + this.autoNotifySave = true, + this.onNotifyClick, + this.courseNotifySaveKey = ApConstants.semesterLatest, + this.customStateHint, + this.itemPicker, + this.onSearchButtonClick, + this.enableAddToCalendar = true, + this.androidResourceIcon, + this.enableCaptureCourseTable = false, + this.showSectionTime, + this.showInstructors, + this.showClassroomLocation, + this.showSearchButton, + }); + + /// Creates from a [DataState]. + GlassCourseScaffold.fromDataState({ + super.key, + required DataState dataState, + this.title, + this.semesterData, + this.onSelect, + this.onRefresh, + this.actions, + this.enableNotifyControl = true, + this.notifyData, + this.autoNotifySave = true, + this.onNotifyClick, + this.courseNotifySaveKey = ApConstants.semesterLatest, + this.itemPicker, + this.onSearchButtonClick, + this.enableAddToCalendar = true, + this.androidResourceIcon, + this.enableCaptureCourseTable = false, + this.showSectionTime, + this.showInstructors, + this.showClassroomLocation, + this.showSearchButton, + }) : state = dataState.when( + loading: () => CourseState.loading, + loaded: (_, __) => CourseState.finish, + error: (_) => CourseState.error, + empty: (_) => CourseState.empty, + ), + courseData = dataState.dataOrNull ?? CourseData.empty(), + customHint = dataState is DataLoaded ? dataState.hint : null, + customStateHint = dataState is DataError + ? dataState.hint + : dataState is DataEmpty + ? dataState.hint + : null; + + final CourseState state; + final CourseData courseData; + final String? title; + final String? customHint; + final SemesterData? semesterData; + final Function(int index)? onSelect; + final Future? Function()? onRefresh; + final List? actions; + final bool enableNotifyControl; + final CourseNotifyData? notifyData; + final bool autoNotifySave; + final CourseNotifyCallback? onNotifyClick; + final String courseNotifySaveKey; + final String? customStateHint; + final Widget? itemPicker; + final Function()? onSearchButtonClick; + final bool enableAddToCalendar; + final String? androidResourceIcon; + final bool enableCaptureCourseTable; + final bool? showSectionTime; + final bool? showInstructors; + final bool? showClassroomLocation; + final bool? showSearchButton; + + @override + GlassCourseScaffoldState createState() => GlassCourseScaffoldState(); +} + +class GlassCourseScaffoldState extends State { + final GlobalKey _repaintBoundaryGlobalKey = GlobalKey(); + + ContentStyle _contentStyle = ContentStyle.table; + + bool? showSectionTime; + bool? showInstructors; + bool? showClassroomLocation; + bool? showSearchButton; + bool? mergeCourse; + + bool get isLandscape => + MediaQuery.of(context).orientation == Orientation.landscape; + + List invisibleCourseCodes = []; + + final Map _courseColorMap = {}; + int _colorIndex = 0; + + Color _getCourseColor(String courseCode) { + if (!_courseColorMap.containsKey(courseCode)) { + _courseColorMap[courseCode] = + glassCourseColors[_colorIndex % glassCourseColors.length]; + _colorIndex++; + } + return _courseColorMap[courseCode]!; + } + + late ScrollController _scrollController; + bool _showFab = true; + + @override + void initState() { + showSectionTime = + widget.showSectionTime ?? + PreferenceUtil.instance.getBool(ApConstants.showSectionTime, true); + showInstructors = + widget.showInstructors ?? + PreferenceUtil.instance.getBool(ApConstants.showInstructors, true); + showClassroomLocation = + widget.showClassroomLocation ?? + PreferenceUtil.instance.getBool( + ApConstants.showClassroomLocation, + true, + ); + showSearchButton = + widget.showSearchButton ?? + PreferenceUtil.instance.getBool( + ApConstants.showCourseSearchButton, + true, + ); + mergeCourse = PreferenceUtil.instance.getBool( + '${ApConstants.packageName}.merge_course', + true, + ); + fetchInvisibleCourseCodes(); + _scrollController = ScrollController(); + _scrollController.addListener(() { + if (_scrollController.position.userScrollDirection == + ScrollDirection.reverse) { + if (_showFab) { + setState(() => _showFab = false); + } + } else if (_scrollController.position.userScrollDirection == + ScrollDirection.forward) { + if (!_showFab) { + setState(() => _showFab = true); + } + } + }); + super.initState(); + } + + @override + void didUpdateWidget(covariant GlassCourseScaffold oldWidget) { + fetchInvisibleCourseCodes(); + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AdaptiveLiquidGlassLayer( + child: CourseConfig( + showSectionTime: showSectionTime, + showInstructors: showInstructors, + showClassroomLocation: showClassroomLocation, + child: Scaffold( + body: Stack( + children: [ + Padding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + 50, + ), + child: Row( + children: [ + Expanded( + flex: 3, + child: Flex( + direction: Axis.vertical, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (widget.customHint != null && + widget.customHint!.isNotEmpty) + HintBanner(text: widget.customHint!), + Expanded( + child: RefreshIndicator( + onRefresh: () async { + await widget.onRefresh!(); + AnalyticsUtil.instance.logEvent( + 'course_refresh', + ); + return; + }, + child: _body(), + ), + ), + ], + ), + ), + if (widget.state == CourseState.finish && + isLandscape) ...[ + const SizedBox(width: 16.0), + Expanded( + flex: 2, + child: GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: GlassCourseList( + courses: widget.courseData.courses, + timeCodes: widget.courseData.timeCodes, + invisibleCourseCodes: invisibleCourseCodes, + onVisibilityChanged: + (Course course, bool visibility) => + saveInvisibleCourseCodes( + course: course, + visibility: visibility, + ), + ), + ), + ), + ], + ], + ), + ), + GlassFloatingToolbar( + leading: [ + if (widget.semesterData != null && widget.itemPicker == null) + GlassSemesterPicker( + semesterData: widget.semesterData!, + currentIndex: widget.semesterData!.currentIndex, + onSelect: (Semester semester, int index) { + widget.onSelect?.call(index); + }, + featureTag: 'course', + ) + else if (widget.itemPicker != null) + widget.itemPicker!, + ], + trailing: [ + ...widget.actions ?? [], + if (widget.enableCaptureCourseTable) + IconButton( + icon: Icon(ApIcon.download), + onPressed: _captureCourseTable, + iconSize: 22, + padding: EdgeInsets.zero, + constraints: + const BoxConstraints( + minWidth: 36, + minHeight: 36, + ), + ), + IconButton( + icon: Icon(ApIcon.settings), + onPressed: _openSettings, + iconSize: 22, + padding: EdgeInsets.zero, + constraints: + const BoxConstraints( + minWidth: 36, + minHeight: 36, + ), + ), + ], + ), + ], + ), + floatingActionButton: AnimatedScale( + scale: _showFab ? 1.0 : 0.0, + duration: const Duration(milliseconds: 250), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (!isLandscape) + GlassButton( + key: const ValueKey('switch_content_style_button'), + icon: Icon( + _contentStyle == ContentStyle.table + ? Icons.list_rounded + : Icons.grid_view_rounded, + ), + onTap: () { + setState( + () => _contentStyle = + (_contentStyle == ContentStyle.table) + ? ContentStyle.list + : ContentStyle.table, + ); + }, + useOwnLayer: true, + ), + if (showSearchButton ?? true) ...[ + if (!isLandscape) const SizedBox(height: 8), + GlassButton( + key: const ValueKey('search_button'), + icon: const Icon(Icons.search), + onTap: () { + _pickSemester(); + AnalyticsUtil.instance.logEvent( + 'course_search_button_click', + ); + }, + useOwnLayer: true, + ), + ], + ], + ), + ), + ), + ), + ); + } + + Widget _buildErrorState( + ColorScheme colorScheme, + String message, + IconData icon, + ) { + return InkWell( + onTap: () { + if (widget.state == CourseState.empty) { + _pickSemester(); + } else { + widget.onRefresh?.call(); + } + }, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + GlassCard( + useOwnLayer: true, + width: 80, + height: 80, + padding: EdgeInsets.zero, + shape: const LiquidRoundedSuperellipse(borderRadius: 20), + child: Center( + child: Icon(icon, size: 40, color: colorScheme.primary), + ), + ), + const SizedBox(height: 16), + Text( + message, + style: TextStyle( + fontSize: 16, + color: colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + context.ap.clickToRetry, + style: TextStyle(fontSize: 14, color: colorScheme.primary), + ), + ], + ), + ), + ); + } + + Widget _body() { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + switch (widget.state) { + case CourseState.loading: + return const Center(child: GlassProgressIndicator.circular()); + case CourseState.error: + return _buildErrorState( + colorScheme, + context.ap.clickToRetry, + Icons.error_outline_rounded, + ); + case CourseState.empty: + return _buildErrorState( + colorScheme, + context.ap.courseEmpty, + Icons.event_busy_rounded, + ); + case CourseState.offlineEmpty: + return _buildErrorState( + colorScheme, + context.ap.noOfflineData, + Icons.cloud_off_rounded, + ); + case CourseState.custom: + return _buildErrorState( + colorScheme, + widget.customStateHint ?? context.ap.unknownError, + Icons.warning_amber_rounded, + ); + default: + if (isLandscape || _contentStyle == ContentStyle.table) { + return GlassCourseTableView( + courseData: widget.courseData, + invisibleCourseCodes: invisibleCourseCodes, + controller: _scrollController, + onCoursePressed: (Course course, TimeCode timeCode, int weekday) => + _onPressed(weekday, timeCode, course), + courseColorResolver: _getCourseColor, + repaintBoundaryKey: _repaintBoundaryGlobalKey, + mergeCourse: mergeCourse, + showSectionTime: showSectionTime, + showInstructors: showInstructors, + showClassroomLocation: showClassroomLocation, + ); + } else { + return GlassCourseList( + controller: _scrollController, + courses: widget.courseData.courses, + timeCodes: widget.courseData.timeCodes, + invisibleCourseCodes: invisibleCourseCodes, + onVisibilityChanged: (Course course, bool visibility) => + saveInvisibleCourseCodes( + course: course, + visibility: visibility, + ), + ); + } + } + } + + Future _captureCourseTable() async { + final RenderRepaintBoundary? boundary = + _repaintBoundaryGlobalKey.currentContext!.findRenderObject() + as RenderRepaintBoundary?; + if (boundary == null) { + UiUtil.instance.showToast(context, context.ap.unknownError); + return; + } + final ui.Image image = await boundary.toImage(pixelRatio: 3.0); + final ByteData? byteData = await image.toByteData( + format: ui.ImageByteFormat.png, + ); + final DateTime now = DateTime.now(); + final String formattedDate = DateFormat('yyyyMMdd_hhmmss').format(now); + if (byteData != null) { + final SaveImageResult result = await MediaUtil.instance.saveImage( + byteData: byteData, + fileName: 'course_table_$formattedDate', + ); + if (!mounted) return; + switch (result) { + case SaveImageSuccess(:final String filePath): + final String message = + '${context.ap.exportCourseTableSuccess}' + '\n$filePath'; + Toast.show(message, context); + AnalyticsUtil.instance.logEvent('export_course_table_image_success'); + case SaveImageError(:final String message): + UiUtil.instance.showToast(context, message); + } + } else { + if (!mounted) return; + UiUtil.instance.showToast(context, context.ap.unknownError); + } + } + + void _onPressed(int weekday, TimeCode timeCode, Course course) { + GlassSheet.show( + context: context, + isScrollControlled: true, + builder: (BuildContext builder) { + return GlassCourseContent( + enableNotifyControl: widget.enableNotifyControl, + course: course, + notifyData: widget.notifyData, + weekday: weekday, + courseNotifySaveKey: widget.courseNotifySaveKey, + timeCode: timeCode, + courseColor: _getCourseColor(course.code), + invisibleCourseCodes: invisibleCourseCodes, + onVisibilityChanged: (bool visibility) => + saveInvisibleCourseCodes(course: course, visibility: visibility), + ); + }, + ); + } + + void _pickSemester() { + if (widget.semesterData != null) { + GlassSemesterPicker.show( + context: context, + semesterData: widget.semesterData!, + currentIndex: widget.semesterData!.currentIndex, + onSelect: (Semester semester, int index) { + widget.onSelect?.call(index); + }, + ); + } + widget.onSearchButtonClick?.call(); + } + + void _openSettings() { + showGlassDefaultDialog( + context: context, + title: context.ap.courseScaffoldSetting, + actionText: context.ap.confirm, + actionFunction: () { + Navigator.of(context, rootNavigator: true).pop(); + }, + contentWidget: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CheckboxListTile( + title: Text(context.ap.showSectionTime), + secondary: Icon(ApIcon.accessTime), + value: showSectionTime, + onChanged: (bool? value) { + setState(() => showSectionTime = value); + PreferenceUtil.instance.setBool( + ApConstants.showSectionTime, + showSectionTime!, + ); + }, + ), + CheckboxListTile( + title: Text(context.ap.showInstructors), + secondary: Icon(ApIcon.person), + value: showInstructors, + onChanged: (bool? value) { + setState(() => showInstructors = value); + PreferenceUtil.instance.setBool( + ApConstants.showInstructors, + showInstructors!, + ); + }, + ), + CheckboxListTile( + title: Text(context.ap.showClassroomLocation), + secondary: Icon(ApIcon.locationOn), + value: showClassroomLocation, + onChanged: (bool? value) { + setState(() => showClassroomLocation = value); + PreferenceUtil.instance.setBool( + ApConstants.showClassroomLocation, + showClassroomLocation!, + ); + }, + ), + CheckboxListTile( + title: Text(context.ap.showSearchButton), + secondary: const Icon(Icons.search), + value: showSearchButton, + onChanged: (bool? value) { + setState(() => showSearchButton = value); + PreferenceUtil.instance.setBool( + ApConstants.showCourseSearchButton, + showSearchButton!, + ); + }, + ), + CheckboxListTile( + title: Text(context.ap.mergeCourse), + secondary: const Icon(Icons.merge_type_rounded), + value: mergeCourse, + onChanged: (bool? value) { + setState(() => mergeCourse = value); + PreferenceUtil.instance.setBool( + '${ApConstants.packageName}' + '.merge_course', + mergeCourse!, + ); + }, + ), + ], + ), + ); + AnalyticsUtil.instance.logEvent('course_setting_click'); + } + + void saveInvisibleCourseCodes({ + required Course course, + required bool visibility, + }) { + if (visibility) { + invisibleCourseCodes.remove(course.code); + } else { + invisibleCourseCodes.add(course.code); + } + PreferenceUtil.instance.setStringList( + '$_kCourseInvisibleKey' + '${widget.courseNotifySaveKey}', + invisibleCourseCodes, + ); + setState(() {}); + } + + void fetchInvisibleCourseCodes() { + invisibleCourseCodes = PreferenceUtil.instance.getStringList( + '$_kCourseInvisibleKey' + '${widget.courseNotifySaveKey}', + [], + ); + setState(() {}); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_home_page_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_home_page_scaffold.dart new file mode 100644 index 00000000..cc2832b2 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_home_page_scaffold.dart @@ -0,0 +1,501 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_dialog.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_floating_toolbar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [HomePageScaffold]. +/// +/// Replaces the Material [AppBar] with [GlassAppBar] and the +/// [NavigationBar] with [GlassBottomBar] while preserving all +/// original functionality (announcement carousel, responsive +/// layout, etc.). +class GlassHomePageScaffold extends StatefulWidget { + const GlassHomePageScaffold({ + super.key, + required this.state, + required this.announcements, + required this.isLogin, + this.actions, + this.onTabTapped, + this.bottomNavigationBarItems, + this.selectedIndex = 0, + this.drawer, + this.content, + this.floatingActionButton, + this.title, + this.onImageTapped, + this.autoPlay = true, + this.autoPlayDuration = + const Duration(milliseconds: 5000), + this.bottomPadding = 0, + }); + + /// Creates from a [DataState>]. + GlassHomePageScaffold.fromDataState({ + super.key, + required DataState> dataState, + required this.isLogin, + this.actions, + this.onTabTapped, + this.bottomNavigationBarItems, + this.selectedIndex = 0, + this.drawer, + this.content, + this.floatingActionButton, + this.title, + this.onImageTapped, + this.autoPlay = true, + this.autoPlayDuration = + const Duration(milliseconds: 5000), + this.bottomPadding = 0, + }) : state = dataState.when( + loading: () => HomeState.loading, + loaded: (_, __) => HomeState.finish, + error: (_) => HomeState.error, + empty: (_) => HomeState.empty, + ), + announcements = + dataState.dataOrNull ?? []; + + final HomeState state; + final String? title; + final List announcements; + final List? actions; + final List? bottomNavigationBarItems; + final int selectedIndex; + final Function(int index)? onTabTapped; + final Function(Announcement announcement)? onImageTapped; + final Widget? drawer; + final Widget? content; + final Widget? floatingActionButton; + final bool isLogin; + final bool autoPlay; + + /// Extra bottom padding for the content area. + /// Use when the scaffold is embedded inside + /// another layout with a floating bottom bar. + final double bottomPadding; + final Duration autoPlayDuration; + + @override + GlassHomePageScaffoldState createState() => + GlassHomePageScaffoldState(); +} + +class GlassHomePageScaffoldState + extends State { + final GlobalKey + _scaffoldMessengerKey = + GlobalKey(); + + PageController? pageController; + int _currentNewsIndex = 0; + Timer? _timer; + + bool get isTablet => + MediaQuery.of(context).size.shortestSide >= 680; + + @override + void initState() { + _setTimer(); + super.initState(); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AdaptiveLiquidGlassLayer( + child: PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, _) { + if (Platform.isAndroid) { + _showLogoutDialog(); + } else { + SystemNavigator.pop(); + } + }, + child: ScaffoldMessenger( + key: _scaffoldMessengerKey, + child: Scaffold( + drawer: isTablet ? null : widget.drawer, + floatingActionButton: + widget.floatingActionButton, + body: Stack( + children: [ + Row( + children: [ + if (isTablet) widget.drawer!, + Expanded( + child: (isTablet && + widget.content != null) + ? widget.content! + : OrientationBuilder( + builder: ( + _, + Orientation orientation, + ) { + return Container( + padding: + EdgeInsets.only( + top: MediaQuery.of( + context, + ).padding.top + + 60 + + (orientation == + Orientation + .portrait + ? 8.0 + : 4.0), + bottom: widget + .bottomNavigationBarItems != + null + ? 72.0 + : widget + .bottomPadding, + ), + alignment: + Alignment.center, + child: _homeBody( + orientation, + ), + ); + }, + ), + ), + ], + ), + if (!isTablet) + GlassFloatingToolbar( + leading: [ + Text( + widget.title ?? '', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + trailing: widget.actions ?? + const [], + ), + if (widget.bottomNavigationBarItems != + null && + !isTablet) + Positioned( + left: 16, + right: MediaQuery.of(context) + .size + .width * + 0.2, + bottom: MediaQuery.of(context) + .padding + .bottom + + 8, + child: Builder( + builder: (BuildContext ctx) { + final bool isDark = + Theme.of(ctx).brightness == + Brightness.dark; + final Color iconColor = isDark + ? Colors.white + : Colors.black; + return GlassBottomBar( + tabs: widget + .bottomNavigationBarItems!, + selectedIndex: + widget.selectedIndex, + onTabSelected: (int index) { + widget.onTabTapped + ?.call(index); + }, + barHeight: 52, + barBorderRadius: 26, + horizontalPadding: 10, + verticalPadding: 10, + iconSize: 20, + selectedIconColor: iconColor, + unselectedIconColor: + iconColor.withAlpha(153), + textStyle: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: iconColor, + ), + ); + }, + ), + ), + ], + ), + ), + ), + ), + ); + } + + void _setTimer() { + if (widget.autoPlay) { + _timer = Timer.periodic( + widget.autoPlayDuration, + (Timer timer) { + if (widget.state == HomeState.finish && + widget.announcements.length > 1 && + pageController!.hasClients) { + setState(() { + _currentNewsIndex++; + if (_currentNewsIndex >= + widget.announcements.length) { + _currentNewsIndex = 0; + } + pageController?.animateToPage( + _currentNewsIndex, + duration: + const Duration(milliseconds: 500), + curve: Curves.easeOutQuint, + ); + }); + } + }, + ); + } + } + + Widget _newsImage( + Announcement announcement, + Orientation orientation, + bool active, + ) { + return GestureDetector( + onTap: () { + widget.onImageTapped?.call(announcement); + AnalyticsUtil.instance + .logEvent('announcement_image_click'); + }, + onTapDown: (TapDownDetails detail) { + _timer?.cancel(); + }, + onTapUp: (TapUpDetails detail) { + _setTimer(); + }, + onTapCancel: () { + _setTimer(); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 500), + curve: Curves.easeOutQuint, + margin: EdgeInsets.symmetric( + vertical: MediaQuery.of(context).size.height * + (active ? 0.05 : 0.15), + horizontal: + MediaQuery.of(context).size.width * 0.02, + ), + child: Hero( + tag: announcement.hashCode, + child: ApNetworkImage( + url: announcement.imgUrl, + ), + ), + ), + ); + } + + Widget _homeBody(Orientation orientation) { + double viewportFraction = 0.65; + if (orientation == Orientation.landscape) { + viewportFraction = 0.5; + } + pageController = PageController( + viewportFraction: viewportFraction, + ); + pageController!.addListener(() { + final int next = pageController!.page!.round(); + if (_currentNewsIndex != next) { + setState(() { + _currentNewsIndex = next; + }); + } + }); + switch (widget.state) { + case HomeState.loading: + return const Center( + child: GlassProgressIndicator.circular(), + ); + case HomeState.finish: + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Hero( + tag: ApConstants.tagAnnouncementTitle, + child: Text( + widget + .announcements[_currentNewsIndex] + .title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20.0, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + ), + ), + const Hero( + tag: ApConstants.tagAnnouncementIcon, + child: Icon(Icons.arrow_drop_down), + ), + Expanded( + child: PageView.builder( + controller: pageController, + itemCount: widget.announcements.length, + itemBuilder: ( + BuildContext context, + int currentIndex, + ) { + final bool active = + currentIndex == _currentNewsIndex; + return _newsImage( + widget.announcements[currentIndex], + orientation, + active, + ); + }, + ), + ), + SizedBox( + height: + orientation == Orientation.portrait + ? 16.0 + : 4.0, + ), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + fontSize: 24.0, + ), + children: [ + TextSpan( + text: '${widget.announcements + .length >= + 10 && + _currentNewsIndex < 9 + ? '0' + : ''}' + '${_currentNewsIndex + 1}', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .primary, + ), + ), + TextSpan( + text: + ' / ${widget.announcements.length}', + ), + ], + ), + ), + SizedBox( + height: + orientation == Orientation.portrait + ? 24.0 + : 0.0, + ), + ], + ); + case HomeState.offline: + return HintContent( + icon: ApIcon.offlineBolt, + content: context.ap.offlineMode, + ); + case HomeState.empty: + return HintContent( + icon: ApIcon.offlineBolt, + content: context.ap.announcementEmpty, + ); + case HomeState.error: + return HintContent( + icon: ApIcon.offlineBolt, + content: context.ap.somethingError, + ); + } + } + + void _showLogoutDialog() { + final ApLocalizations l10n = context.ap; + showGlassYesNoDialog( + context: context, + title: l10n.closeAppTitle, + contentWidget: Text( + l10n.closeAppHint, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + leftActionText: l10n.cancel, + rightActionText: l10n.confirm, + rightActionFunction: () { + AnalyticsUtil.instance + .logEvent('logout_dialog_confirm'); + SystemNavigator.pop(); + }, + ); + AnalyticsUtil.instance + .logEvent('logout_dialog_open'); + } + + /// Hide the current snack bar. + void hideSnackBar() { + _scaffoldMessengerKey.currentState + ?.hideCurrentSnackBar(); + } + + /// Show a short hint snack bar. + void showBasicHint({required String text}) { + showSnackBar( + text: text, + duration: const Duration(seconds: 2), + ); + } + + /// Show a snack bar with optional action. + ScaffoldFeatureController? + showSnackBar({ + required String text, + String? actionText, + Function()? onSnackBarTapped, + Duration? duration, + }) { + return _scaffoldMessengerKey.currentState + ?.showSnackBar( + SnackBar( + content: Text(text), + duration: duration ?? const Duration(days: 1), + action: actionText == null + ? null + : SnackBarAction( + onPressed: onSnackBarTapped!, + label: actionText, + ), + ), + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_image_viewer_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_image_viewer_scaffold.dart new file mode 100644 index 00000000..9236f284 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_image_viewer_scaffold.dart @@ -0,0 +1,108 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_floating_toolbar.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; +import 'package:photo_view/photo_view.dart'; + +/// A glass-enhanced version of [ImageViewerScaffold]. +/// +/// Replaces the [AppBar] with [GlassAppBar]. Uses the standard +/// [TabBar] for multi-image navigation since [GlassTabBar] +/// requires [GlassTab] items and index-based selection rather +/// than a [TabController]. +class GlassImageViewerScaffold extends StatefulWidget { + const GlassImageViewerScaffold({ + super.key, + this.title, + required this.imageViewers, + this.actions, + }); + + final String? title; + final List imageViewers; + final List? actions; + + @override + GlassImageViewerScaffoldState createState() => + GlassImageViewerScaffoldState(); +} + +class GlassImageViewerScaffoldState + extends State + with SingleTickerProviderStateMixin { + TabController? _tabController; + + @override + void initState() { + _tabController = TabController( + vsync: this, + length: widget.imageViewers.length, + ); + super.initState(); + } + + @override + void dispose() { + _tabController!.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AdaptiveLiquidGlassLayer( + child: Scaffold( + body: Stack( + children: [ + Padding( + padding: EdgeInsets.only( + top: + MediaQuery.of(context).padding.top + + 60, + ), + child: Column( + children: [ + if (widget.imageViewers.length > 1) + TabBar( + controller: _tabController, + tabs: [ + for (final ImageViewer image + in widget.imageViewers) + Tab(text: image.title), + ], + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + for (final ImageViewer image + in widget.imageViewers) + PhotoView( + imageProvider: AssetImage( + image.assetName, + ), + ), + ], + ), + ), + ], + ), + ), + GlassFloatingToolbar( + leading: [ + Text( + widget.title ?? '', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + trailing: + widget.actions ?? const [], + ), + ], + ), + ), + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_login_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_login_scaffold.dart new file mode 100644 index 00000000..5becb9d3 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_login_scaffold.dart @@ -0,0 +1,164 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [LoginScaffold]. +/// +/// Replaces the gradient background with a translucent glass +/// surface and the form card with [GlassCard]. +class GlassLoginScaffold extends StatefulWidget { + const GlassLoginScaffold({ + super.key, + required this.logoSource, + required this.forms, + this.logoMode = LogoMode.text, + }); + + final LogoMode logoMode; + final String logoSource; + final List forms; + + static const String routerName = '/login'; + + @override + GlassLoginScaffoldState createState() => + GlassLoginScaffoldState(); +} + +class GlassLoginScaffoldState + extends State { + bool get isTablet => + MediaQuery.of(context).size.shortestSide >= 600; + + @override + Widget build(BuildContext context) { + final Orientation orientation = + MediaQuery.of(context).orientation; + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final bool isDark = + Theme.of(context).brightness == Brightness.dark; + + return AdaptiveLiquidGlassLayer( + child: Scaffold( + resizeToAvoidBottomInset: + orientation == Orientation.portrait, + body: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: isDark + ? [ + colorScheme.surface, + colorScheme.surfaceContainerLowest, + ] + : [ + colorScheme.primary, + colorScheme.primaryContainer, + ], + ), + ), + child: AutofillGroup( + child: KeyboardDismissOnTap( + child: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 32.0, + ), + child: orientation == + Orientation.portrait + ? Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + mainAxisAlignment: + MainAxisAlignment.center, + children: _renderContent( + orientation, + colorScheme, + isDark, + ), + ) + : Row( + mainAxisAlignment: + MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.start, + children: _renderContent( + orientation, + colorScheme, + isDark, + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); + } + + Widget _logo(ColorScheme colorScheme, bool isDark) { + switch (widget.logoMode) { + case LogoMode.image: + return ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image.asset( + widget.logoSource, + width: 80, + height: 80, + fit: BoxFit.contain, + ), + ); + case LogoMode.text: + return TextLogo( + text: widget.logoSource, + isDark: isDark, + colorScheme: colorScheme, + ); + } + } + + List _renderContent( + Orientation orientation, + ColorScheme colorScheme, + bool isDark, + ) { + final Widget logoWidget = + _logo(colorScheme, isDark); + final Widget formCard = ConstrainedBox( + constraints: + const BoxConstraints(maxWidth: 400), + child: GlassCard( + useOwnLayer: true, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 24, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: widget.forms, + ), + ), + ); + + if (orientation == Orientation.portrait) { + return [ + Center(child: logoWidget), + const SizedBox(height: 48), + formCard, + ]; + } else { + return [ + Expanded(child: Center(child: logoWidget)), + const SizedBox(width: 32), + Expanded(child: Center(child: formCard)), + ]; + } + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_score_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_score_scaffold.dart new file mode 100644 index 00000000..cc330e4f --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_score_scaffold.dart @@ -0,0 +1,360 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_floating_toolbar.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_score_analysis_tab.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_score_list_tab.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_semester_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [ScoreScaffold]. +/// +/// Replaces the Material [AppBar] with [GlassAppBar] and the +/// [FloatingActionButton]s with [GlassButton] while preserving +/// all original functionality (analysis view toggle, scroll-aware +/// FAB visibility, responsive landscape layout, etc.). +class GlassScoreScaffold extends StatefulWidget { + const GlassScoreScaffold({ + super.key, + required this.state, + required this.scoreData, + required this.onRefresh, + this.title, + this.itemPicker, + this.semesterData, + this.onSelect, + this.onSearchButtonClick, + this.middleTitle, + this.finalTitle, + this.onScoreSelect, + this.middleScoreBuilder, + this.finalScoreBuilder, + this.customHint, + this.isShowSearchButton = false, + this.bottom, + this.customStateHint, + }); + + /// Creates from a [DataState]. + GlassScoreScaffold.fromDataState({ + super.key, + required DataState dataState, + required this.onRefresh, + this.title, + this.itemPicker, + this.semesterData, + this.onSelect, + this.onSearchButtonClick, + this.middleTitle, + this.finalTitle, + this.onScoreSelect, + this.middleScoreBuilder, + this.finalScoreBuilder, + this.isShowSearchButton = false, + this.bottom, + }) : state = dataState.when( + loading: () => ScoreState.loading, + loaded: (_, __) => ScoreState.finish, + error: (_) => ScoreState.error, + empty: (_) => ScoreState.empty, + ), + scoreData = dataState.dataOrNull, + customHint = dataState is DataLoaded ? dataState.hint : null, + customStateHint = dataState is DataError + ? dataState.hint + : dataState is DataEmpty + ? dataState.hint + : null; + + final ScoreState state; + final String? customStateHint; + final String? title; + final ScoreData? scoreData; + final SemesterData? semesterData; + final Function(int index)? onSelect; + final Function()? onSearchButtonClick; + final Function()? onRefresh; + final Widget? itemPicker; + final String? middleTitle; + final String? finalTitle; + final Function(int index)? onScoreSelect; + final Widget Function(int index)? middleScoreBuilder; + final Widget Function(int index)? finalScoreBuilder; + final bool isShowSearchButton; + final String? customHint; + final Widget? bottom; + + @override + GlassScoreScaffoldState createState() => GlassScoreScaffoldState(); +} + +class GlassScoreScaffoldState extends State { + bool get isLandscape => + MediaQuery.of(context).orientation == Orientation.landscape; + + bool _isAnalysisView = true; + late ScrollController _scrollController; + bool _showFab = true; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + _scrollController.addListener(() { + if (_scrollController.position.userScrollDirection == + ScrollDirection.reverse) { + if (_showFab) { + setState(() => _showFab = false); + } + } else if (_scrollController.position.userScrollDirection == + ScrollDirection.forward) { + if (!_showFab) { + setState(() => _showFab = true); + } + } + }); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + + return AdaptiveLiquidGlassLayer( + child: Scaffold( + floatingActionButton: AnimatedScale( + scale: _showFab ? 1.0 : 0.0, + duration: const Duration(milliseconds: 250), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (widget.state == ScoreState.finish && + widget.scoreData != null && + widget.scoreData!.scores.isNotEmpty && + !isLandscape) + GlassButton( + key: const ValueKey('switch_view_button'), + icon: Icon( + _isAnalysisView + ? Icons.list_alt_rounded + : Icons.analytics_outlined, + ), + onTap: () { + setState(() => _isAnalysisView = !_isAnalysisView); + }, + ), + if (widget.isShowSearchButton) ...[ + if (widget.state == ScoreState.finish && + widget.scoreData != null && + widget.scoreData!.scores.isNotEmpty && + !isLandscape) + const SizedBox(height: 8), + GlassButton( + key: const ValueKey('search_button'), + icon: const Icon(Icons.search), + onTap: () { + _pickSemester(); + AnalyticsUtil.instance.logEvent( + 'score_search_button_click', + ); + }, + ), + ], + ], + ), + ), + body: Stack( + children: [ + Padding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + 60, + ), + child: Row( + children: [ + Expanded( + flex: 3, + child: Column( + children: [ + if (widget.customHint != null && + widget.customHint!.isNotEmpty) + HintBanner(text: widget.customHint!), + Expanded(child: _buildContent(context, colorScheme)), + ], + ), + ), + if (widget.state == ScoreState.finish && + isLandscape) ...[ + const SizedBox(width: 16.0), + Expanded( + flex: 2, + child: GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: GlassScoreListTab( + scoreData: widget.scoreData!, + onRefresh: widget.onRefresh, + middleTitle: widget.middleTitle, + finalTitle: widget.finalTitle, + onScoreSelect: widget.onScoreSelect, + middleScoreBuilder: widget.middleScoreBuilder, + finalScoreBuilder: widget.finalScoreBuilder, + ), + ), + ), + ], + ], + ), + ), + GlassFloatingToolbar( + leading: [ + if (widget.semesterData != null && widget.itemPicker == null) + GlassSemesterPicker( + semesterData: widget.semesterData!, + currentIndex: widget.semesterData!.currentIndex, + onSelect: (Semester semester, int index) { + widget.onSelect?.call(index); + }, + featureTag: 'score', + ) + else if (widget.itemPicker != null) + widget.itemPicker!, + ], + ), + ], + ), + ), + ); + } + + Widget _buildContent(BuildContext context, ColorScheme colorScheme) { + switch (widget.state) { + case ScoreState.loading: + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const GlassProgressIndicator.circular(), + const SizedBox(height: 16), + Text( + context.ap.loading, + style: TextStyle( + fontSize: 16, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); + case ScoreState.error: + return _buildErrorState( + colorScheme, + context.ap.clickToRetry, + Icons.error_outline_rounded, + ); + case ScoreState.empty: + return _buildErrorState( + colorScheme, + context.ap.scoreEmpty, + Icons.assignment_outlined, + ); + case ScoreState.offlineEmpty: + return _buildErrorState( + colorScheme, + context.ap.noOfflineData, + Icons.cloud_off_rounded, + ); + case ScoreState.custom: + return _buildErrorState( + colorScheme, + widget.customStateHint ?? context.ap.somethingError, + Icons.warning_amber_rounded, + ); + case ScoreState.finish: + if (widget.scoreData == null) { + return const SizedBox.shrink(); + } + if (isLandscape || _isAnalysisView) { + return GlassScoreAnalysisTab( + scoreData: widget.scoreData!, + onRefresh: widget.onRefresh, + controller: _scrollController, + ); + } + return GlassScoreListTab( + scoreData: widget.scoreData!, + onRefresh: widget.onRefresh, + middleTitle: widget.middleTitle, + finalTitle: widget.finalTitle, + onScoreSelect: widget.onScoreSelect, + middleScoreBuilder: widget.middleScoreBuilder, + finalScoreBuilder: widget.finalScoreBuilder, + controller: _scrollController, + ); + } + } + + Widget _buildErrorState( + ColorScheme colorScheme, + String message, + IconData icon, + ) { + return InkWell( + onTap: () { + if (widget.state == ScoreState.empty) { + _pickSemester(); + } else { + widget.onRefresh?.call(); + } + }, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + GlassCard( + useOwnLayer: true, + width: 80, + height: 80, + child: Icon(icon, size: 40, color: colorScheme.primary), + ), + const SizedBox(height: 16), + Text( + message, + style: TextStyle( + fontSize: 16, + color: colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + if (widget.state != ScoreState.empty) ...[ + const SizedBox(height: 8), + Text( + context.ap.clickToRetry, + style: TextStyle(fontSize: 14, color: colorScheme.primary), + ), + ], + ], + ), + ), + ); + } + + void _pickSemester() { + if (widget.semesterData != null) { + GlassSemesterPicker.show( + context: context, + semesterData: widget.semesterData!, + currentIndex: widget.semesterData!.currentIndex, + onSelect: (Semester semester, int index) { + widget.onSelect?.call(index); + }, + ); + } + widget.onSearchButtonClick?.call(); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_user_info_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_user_info_scaffold.dart new file mode 100644 index 00000000..236538ee --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_user_info_scaffold.dart @@ -0,0 +1,373 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_floating_toolbar.dart'; +import 'package:barcode_widget/barcode_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [UserInfoScaffold]. +/// +/// Replaces the [SliverAppBar] gradient header with a +/// [GlassPanel] overlay and wraps info cards with [GlassCard]. +class GlassUserInfoScaffold extends StatefulWidget { + const GlassUserInfoScaffold({ + super.key, + required this.userInfo, + this.heroTag, + this.actions, + this.onRefresh, + this.enableBarCode = false, + }); + + final UserInfo userInfo; + final String? heroTag; + final List? actions; + final Future Function()? onRefresh; + final bool enableBarCode; + + @override + GlassUserInfoScaffoldState createState() => + GlassUserInfoScaffoldState(); +} + +class GlassUserInfoScaffoldState + extends State { + BarCodeMode codeMode = BarCodeMode.qrCode; + bool _isRefreshing = false; + + String get iconName { + switch (codeMode) { + case BarCodeMode.code39: + return ApImageIcons.qrcode; + case BarCodeMode.qrCode: + return ApImageIcons.barcode; + } + } + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final bool isDark = + Theme.of(context).brightness == Brightness.dark; + + return AdaptiveLiquidGlassLayer( + child: Scaffold( + body: Stack( + children: [ + RefreshIndicator( + onRefresh: () async { + await _handleRefresh(); + AnalyticsUtil.instance + .logEvent('user_info_refresh'); + }, + child: SingleChildScrollView( + physics: + const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + _buildGlassHeader( + colorScheme, + isDark, + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _buildInfoCard(colorScheme), + if (widget.enableBarCode) ...[ + const SizedBox(height: 16), + _buildBarcodeCard(colorScheme), + ], + ], + ), + ), + ], + ), + ), + ), + GlassFloatingToolbar( + trailing: [ + ...widget.actions ?? [], + if (widget.enableBarCode) + IconButton( + icon: Image.asset( + iconName, + height: 20.0, + width: 20.0, + ), + onPressed: () { + setState( + () => codeMode = + BarCodeMode.values[ + (codeMode.index + + 1) % + BarCodeMode + .values + .length], + ); + AnalyticsUtil.instance + .logEvent( + 'user_info_barcode_switch', + ); + }, + iconSize: 22, + padding: EdgeInsets.zero, + constraints: + const BoxConstraints( + minWidth: 36, + minHeight: 36, + ), + ), + IconButton( + icon: _isRefreshing + ? const SizedBox( + width: 20, + height: 20, + child: + CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon( + Icons.refresh_rounded, + ), + onPressed: _isRefreshing + ? null + : _handleRefresh, + iconSize: 22, + padding: EdgeInsets.zero, + constraints: + const BoxConstraints( + minWidth: 36, + minHeight: 36, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildGlassHeader( + ColorScheme colorScheme, + bool isDark, + ) { + final bool hasImage = + widget.userInfo.pictureBytes != null && + widget.userInfo.pictureBytes!.isNotEmpty; + return GlassPanel( + child: Container( + width: double.infinity, + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + 56, + bottom: 24, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: isDark + ? [ + colorScheme.primaryContainer + .withAlpha(128), + colorScheme.surface.withAlpha(128), + ] + : [ + colorScheme.primary.withAlpha(128), + colorScheme.primaryContainer + .withAlpha(128), + ], + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isDark + ? colorScheme.primary + : colorScheme.onPrimary + .withAlpha(128), + width: 4, + ), + ), + child: CircleAvatar( + radius: 48, + backgroundColor: isDark + ? colorScheme.primaryContainer + : colorScheme.onPrimary + .withAlpha(51), + backgroundImage: hasImage + ? MemoryImage( + widget.userInfo.pictureBytes!, + ) + : null, + child: hasImage + ? null + : Icon( + Icons.person_rounded, + size: 56, + color: isDark + ? colorScheme.primary + : colorScheme.onPrimary, + ), + ), + ), + const SizedBox(height: 12), + Text( + widget.userInfo.name ?? '', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: isDark + ? colorScheme.onSurface + : colorScheme.onPrimary, + ), + ), + ], + ), + ), + ); + } + + Widget _buildInfoCard(ColorScheme colorScheme) { + return GlassCard( + child: Column( + children: [ + InfoRow( + icon: Icons.badge_outlined, + title: context.ap.studentId, + value: widget.userInfo.id, + ), + _buildDivider(colorScheme), + if (widget.userInfo.educationSystem != + null) ...[ + InfoRow( + icon: Icons.school_outlined, + title: context.ap.educationSystem, + value: + widget.userInfo.educationSystem!, + ), + _buildDivider(colorScheme), + ], + if (widget.userInfo.email != + null) ...[ + InfoRow( + icon: Icons.email_outlined, + title: context.ap.email, + value: widget.userInfo.email!, + ), + _buildDivider(colorScheme), + ], + InfoRow( + icon: Icons.domain_outlined, + title: context.ap.department, + value: widget.userInfo.department ?? '', + ), + _buildDivider(colorScheme), + InfoRow( + icon: Icons.class_outlined, + title: context.ap.studentClass, + value: widget.userInfo.className ?? '', + ), + ], + ), + ); + } + + Widget _buildBarcodeCard(ColorScheme colorScheme) { + return GlassCard( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + Row( + children: [ + Icon( + Icons.badge_outlined, + color: colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + '學號條碼', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + ], + ), + const SizedBox(height: 16), + GlassContainer( + width: double.infinity, + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: 16, + ), + child: Column( + children: [ + Text( + widget.userInfo.id, + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + letterSpacing: 4, + fontFamily: 'monospace', + ), + ), + const SizedBox(height: 12), + BarcodeWidget( + barcode: + codeMode == BarCodeMode.code39 + ? Barcode.code39() + : Barcode.qrCode(), + data: widget.userInfo.id, + color: colorScheme.onSurface, + height: + codeMode == BarCodeMode.code39 + ? 80 + : 160, + width: + codeMode == BarCodeMode.code39 + ? double.infinity + : 160, + ), + const SizedBox(height: 12), + Text( + '可持本條碼於圖書館借書', + style: TextStyle( + fontSize: 12, + color: colorScheme + .onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildDivider(ColorScheme colorScheme) { + return const GlassDivider( + indent: 72, + ); + } + + Future _handleRefresh() async { + if (widget.onRefresh == null) return; + setState(() => _isRefreshing = true); + await widget.onRefresh!(); + if (mounted) { + setState(() => _isRefreshing = false); + } + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_ap_drawer.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_ap_drawer.dart new file mode 100644 index 00000000..4533c12c --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_ap_drawer.dart @@ -0,0 +1,194 @@ +import 'package:ap_common_flutter_core/ap_common_flutter_core.dart'; +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [ApDrawer]. +/// +/// Uses [GlassPanel] for the header section while preserving +/// the original menu item structure. Wrapped in a standard +/// [Drawer] since [GlassSideBar] expects navigation items +/// rather than arbitrary widget lists. +class GlassApDrawer extends StatelessWidget { + const GlassApDrawer({ + super.key, + required this.onTapHeader, + required this.widgets, + this.userInfo, + this.imageAsset, + this.imageHeroTag = ApConstants.tagStudentPicture, + this.displayPicture = false, + }); + + final UserInfo? userInfo; + final VoidCallback onTapHeader; + final String? imageAsset; + final List widgets; + final String imageHeroTag; + final bool displayPicture; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final bool isDark = + Theme.of(context).brightness == Brightness.dark; + + return Drawer( + backgroundColor: const Color(0x00000000), + elevation: 0, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.horizontal( + right: Radius.circular(20), + ), + ), + child: Material( + color: colorScheme.surface, + borderRadius: const BorderRadius.horizontal( + right: Radius.circular(20), + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + _buildHeader( + context, + colorScheme, + isDark, + ), + Expanded( + child: ListView( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + children: widgets, + ), + ), + ], + ), + ), + ); + } + + Widget _buildHeader( + BuildContext context, + ColorScheme colorScheme, + bool isDark, + ) { + return GlassPanel( + padding: EdgeInsets.zero, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isDark + ? [ + colorScheme.primaryContainer + .withAlpha(128), + colorScheme.surface.withAlpha(128), + ] + : [ + colorScheme.primary.withAlpha(128), + colorScheme.primaryContainer + .withAlpha(128), + ], + ), + ), + child: SafeArea( + bottom: false, + child: InkWell( + onTap: onTapHeader, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 24, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + _buildAvatar( + context, + colorScheme, + isDark, + ), + const SizedBox(height: 12), + Text( + userInfo?.name ?? '', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: isDark + ? colorScheme.onSurface + : colorScheme.onPrimary, + ), + ), + if (userInfo?.id.isNotEmpty ?? + false) + Text( + userInfo!.id, + style: TextStyle( + fontSize: 14, + color: isDark + ? colorScheme + .onSurfaceVariant + : colorScheme.onPrimary + .withAlpha(204), + ), + ), + if (userInfo?.department != null) + Text( + userInfo!.department!, + style: TextStyle( + fontSize: 13, + color: isDark + ? colorScheme + .onSurfaceVariant + : colorScheme.onPrimary + .withAlpha(179), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _buildAvatar( + BuildContext context, + ColorScheme colorScheme, + bool isDark, + ) { + final bool hasImage = + displayPicture && + userInfo?.pictureBytes != null && + userInfo!.pictureBytes!.isNotEmpty; + + return Hero( + tag: imageHeroTag, + child: CircleAvatar( + radius: 36, + backgroundColor: isDark + ? colorScheme.primaryContainer + : colorScheme.onPrimary.withAlpha(51), + backgroundImage: hasImage + ? MemoryImage(userInfo!.pictureBytes!) + : null, + child: hasImage + ? null + : Icon( + Icons.person_rounded, + size: 40, + color: isDark + ? colorScheme.primary + : colorScheme.onPrimary, + ), + ), + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_content.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_content.dart new file mode 100644 index 00000000..ef16de3c --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_content.dart @@ -0,0 +1,496 @@ +import 'dart:io' show Platform; + +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [CourseContent]. +/// +/// Replaces the Material bottom sheet surface with +/// [GlassCard] / [GlassContainer] while keeping all +/// internal logic (notifications, calendar, visibility) +/// identical. +class GlassCourseContent extends StatefulWidget { + /// Creates a [GlassCourseContent]. + const GlassCourseContent({ + super.key, + required this.enableNotifyControl, + required this.course, + required this.timeCode, + required this.weekday, + this.notifyData, + this.autoNotifySave = true, + this.onNotifyClick, + this.courseNotifySaveKey = + ApConstants.semesterLatest, + this.enableAddToCalendar = true, + this.androidResourceIcon, + this.invisibleCourseCodes = const [], + this.onVisibilityChanged, + this.courseColor, + }); + + /// Whether to show notification controls. + final bool enableNotifyControl; + + /// The course to display details for. + final Course course; + + /// The time code for this course slot. + final TimeCode timeCode; + + /// The weekday number for this course slot. + final int weekday; + + /// Notification data for course reminders. + final CourseNotifyData? notifyData; + + /// Whether to auto-save notification changes. + final bool autoNotifySave; + + /// Callback when notification button is pressed. + final CourseNotifyCallback? onNotifyClick; + + /// Key used to persist notification data. + final String courseNotifySaveKey; + + /// Whether to show the add-to-calendar button. + final bool enableAddToCalendar; + + /// Android notification icon resource name. + final String? androidResourceIcon; + + /// Course codes that are currently hidden. + final List invisibleCourseCodes; + + /// Called when visibility is toggled. + final ValueChanged? onVisibilityChanged; + + /// Color for the course header. + final Color? courseColor; + + @override + State createState() => + _GlassCourseContentState(); +} + +class _GlassCourseContentState + extends State { + CourseNotifyData? _notifyData; + + @override + void initState() { + _notifyData = widget.notifyData; + super.initState(); + } + + @override + void didUpdateWidget( + covariant GlassCourseContent oldWidget, + ) { + if (widget.notifyData != oldWidget.notifyData) { + _notifyData = widget.notifyData; + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + CourseNotifyState? state; + if (widget.enableNotifyControl && + widget.notifyData != null) { + state = widget.notifyData!.getByCode( + widget.course.code, + widget.timeCode.startTime, + widget.weekday, + ) == + null + ? CourseNotifyState.cancel + : CourseNotifyState.schedule; + } + final bool visibility = !widget + .invisibleCourseCodes + .contains(widget.course.code); + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final Color courseColor = widget.courseColor ?? + courseColors[widget.course.code.hashCode % + courseColors.length]; + final Color onCourseColor = + ThemeData.estimateBrightnessForColor( + courseColor) == + Brightness.dark + ? Colors.white + : Colors.black; + return GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + shape: const LiquidRoundedSuperellipse( + borderRadius: 24, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.only( + left: 20, + right: 8, + top: 12, + bottom: 20, + ), + decoration: BoxDecoration( + color: courseColor, + borderRadius: + const BorderRadius.vertical( + top: Radius.circular(24), + ), + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + _buildActionRow( + context, + state, + visibility, + onCourseColor, + ), + Row( + children: [ + Expanded( + child: Text( + widget.course.title, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: onCourseColor, + ), + ), + ), + if (widget.course.required != + null && + widget.course.required! + .isNotEmpty) + Container( + padding: + const EdgeInsets + .symmetric( + horizontal: 12, + vertical: 4, + ), + decoration: BoxDecoration( + color: onCourseColor + .withAlpha(51), + borderRadius: + BorderRadius.circular( + 12, + ), + ), + child: Text( + widget.course.required!, + style: TextStyle( + fontSize: 12, + color: onCourseColor, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + widget.course.code, + style: TextStyle( + fontSize: 14, + color: onCourseColor + .withAlpha(204), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + _buildDetailRow( + Icons.person_outline_rounded, + context.ap.courseDialogProfessor, + widget.course.getInstructors(), + colorScheme, + ), + _buildDetailRow( + Icons.location_on_outlined, + context.ap.courseDialogLocation, + widget.course.location + ?.toString() ?? + '-', + colorScheme, + ), + _buildDetailRow( + Icons.school_outlined, + '學分數', + '${widget.course.units ?? "-"}' + ' ${context.ap.units}', + colorScheme, + ), + _buildDetailRow( + Icons.schedule_outlined, + context.ap.courseDialogTime, + '${widget.timeCode.startTime}' + '-${widget.timeCode.endTime}', + colorScheme, + ), + if (widget.course.className != + null && + widget.course.className! + .isNotEmpty) + _buildDetailRow( + Icons.class_outlined, + context.ap.studentClass, + widget.course.className!, + colorScheme, + ), + if (widget.course.hours != null && + widget.course.hours!.isNotEmpty) + _buildDetailRow( + Icons.timer_outlined, + context.ap.courseHours, + widget.course.hours!, + colorScheme, + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildActionRow( + BuildContext context, + CourseNotifyState? state, + bool visibility, + Color onCourseColor, + ) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if ((!kIsWeb && + (Platform.isAndroid || + Platform.isIOS)) && + widget.enableAddToCalendar) + GlassIconButton( + icon: Image.asset( + ApImageIcons.calendarImport, + color: onCourseColor, + height: 24.0, + width: 24.0, + ), + onPressed: () { + final DateFormat format = + DateFormat('HH:mm'); + final DateTime startTime = + format.parse( + widget.timeCode.startTime, + ); + final DateTime endTime = + format.parse( + widget.timeCode.endTime, + ); + PlatformCalendarUtil.instance + .addToApp( + title: widget.course.title, + location: widget.course.location + ?.toString() ?? + '', + startDate: startTime + .weekTime(widget.weekday), + endDate: endTime + .weekTime(widget.weekday), + timeZone: 'GMT+8', + ); + AnalyticsUtil.instance.logEvent( + 'course_export_to_calendar', + ); + }, + ), + GlassIconButton( + icon: Icon( + visibility + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + color: onCourseColor, + ), + onPressed: () { + setState(() { + widget.onVisibilityChanged + ?.call(!visibility); + }); + }, + ), + if (widget.enableNotifyControl && + _notifyData != null && + NotificationUtil.instance.isSupport) + GlassIconButton( + icon: Icon( + state == CourseNotifyState.schedule + ? Icons.alarm_on + : Icons.alarm_off, + color: onCourseColor, + ), + onPressed: () async { + await _handleNotifyPressed( + context, + state, + ); + }, + ), + ], + ); + } + + Future _handleNotifyPressed( + BuildContext context, + CourseNotifyState? state, + ) async { + CourseNotify? courseNotify = + _notifyData!.getByCode( + widget.course.code, + widget.timeCode.startTime, + widget.weekday, + ); + if (widget.autoNotifySave) { + if (courseNotify == null) { + courseNotify = CourseNotify.fromCourse( + id: _notifyData!.lastId + 1, + course: widget.course, + weekday: widget.weekday, + timeCode: widget.timeCode, + ); + await NotificationUtil.instance + .scheduleCourseNotify( + context: context, + courseNotify: courseNotify, + weekday: widget.weekday, + androidResourceIcon: + widget.androidResourceIcon, + ); + _notifyData = _notifyData!.copyWith( + lastId: _notifyData!.lastId + 1, + data: [ + ..._notifyData!.data, + courseNotify, + ], + ); + _notifyData! + .save(widget.courseNotifySaveKey); + if (!context.mounted) return; + UiUtil.instance.showToast( + context, + context.ap.courseNotifyHint, + ); + AnalyticsUtil.instance.logEvent( + 'course_notify_schedule', + ); + } else { + await NotificationUtil.instance + .cancelNotify( + id: courseNotify.id, + ); + final List newData = + [ + ..._notifyData!.data, + ]; + newData.removeWhere( + (CourseNotify data) => + data.id == courseNotify!.id, + ); + _notifyData = _notifyData!.copyWith( + data: newData, + ); + _notifyData! + .save(widget.courseNotifySaveKey); + if (!context.mounted) return; + UiUtil.instance.showToast( + context, + context.ap.cancelNotifySuccess, + ); + } + setState(() {}); + AnalyticsUtil.instance.logEvent( + 'course_notify_cancel', + ); + } + if (widget.onNotifyClick != null) { + if (!mounted) return; + widget.onNotifyClick?.call( + courseNotify, + CourseNotifyState.values[ + (state!.index + 1) % + (CourseNotifyState.values.length)], + ); + } + } + + Widget _buildDetailRow( + IconData icon, + String label, + String value, + ColorScheme colorScheme, + ) { + if (value.isEmpty || value == '-') { + return const SizedBox(); + } + return Padding( + padding: + const EdgeInsets.symmetric(vertical: 8), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + width: 36, + height: 36, + child: Center( + child: Icon( + icon, + size: 18, + color: colorScheme.primary, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12, + color: colorScheme + .onSurfaceVariant, + ), + ), + const SizedBox(height: 2), + SelectableText( + value, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: colorScheme.onSurface, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_list.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_list.dart new file mode 100644 index 00000000..e42a0973 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_list.dart @@ -0,0 +1,219 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_course_content.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_course_table_view.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [CourseList]. +/// +/// Replaces Material [Card] surfaces with [GlassCard] for +/// each course item while keeping the layout and +/// interactions identical. +class GlassCourseList extends StatelessWidget { + /// Creates a [GlassCourseList]. + const GlassCourseList({ + super.key, + required this.courses, + this.invisibleCourseCodes = const [], + this.onVisibilityChanged, + this.timeCodes, + this.controller, + }); + + /// The list of courses to display. + final List courses; + + /// Course codes that should be hidden in the grid. + final List invisibleCourseCodes; + + /// Called when a course's visibility is toggled. + final void Function(Course, bool)? + onVisibilityChanged; + + /// Time code definitions. + final List? timeCodes; + + /// Optional scroll controller. + final ScrollController? controller; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return ListView.builder( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.only( + bottom: 80.0, + left: 16.0, + right: 16.0, + top: 16.0, + ), + itemCount: courses.length, + itemBuilder: (_, int index) { + final Course course = courses[index]; + final bool visibility = + !invisibleCourseCodes + .contains(course.code); + final Color courseColor = + glassCourseColors[index % glassCourseColors.length]; + final String instructors = + course.getInstructors(); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: InkWell( + onTap: () { + GlassSheet.show( + context: context, + isScrollControlled: true, + builder: + (BuildContext context) => + GlassCourseContent( + course: course, + invisibleCourseCodes: + invisibleCourseCodes, + onVisibilityChanged: + (bool visible) => + onVisibilityChanged + ?.call( + course, + visible, + ), + timeCode: timeCodes != null && + timeCodes!.isNotEmpty + ? timeCodes![0] + : const TimeCode( + title: '', + startTime: '', + endTime: '', + ), + weekday: course.times.isNotEmpty + ? course.times.first.weekday + : 1, + courseColor: courseColor, + enableNotifyControl: false, + ), + ); + }, + borderRadius: + BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + width: 4, + height: 60, + decoration: BoxDecoration( + color: courseColor, + borderRadius: + BorderRadius.circular(2), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + course.title, + style: TextStyle( + fontSize: 16, + fontWeight: + FontWeight.w600, + color: colorScheme + .onSurface, + ), + ), + const SizedBox(height: 4), + Text( + instructors.isNotEmpty + ? instructors + : context.ap.unknown, + style: TextStyle( + fontSize: 13, + color: colorScheme + .onSurfaceVariant, + ), + ), + Text( + course.location + ?.toString() ?? + '-', + style: TextStyle( + fontSize: 13, + color: colorScheme + .onSurfaceVariant, + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + GlassIconButton( + size: 32, + icon: Icon( + visibility + ? Icons + .visibility_outlined + : Icons + .visibility_off_outlined, + size: 20, + color: colorScheme + .onSurfaceVariant, + ), + onPressed: () => + onVisibilityChanged + ?.call( + course, + !visibility, + ), + ), + const SizedBox(height: 12), + Container( + padding: + const EdgeInsets + .symmetric( + horizontal: 10, + vertical: 4, + ), + decoration: BoxDecoration( + color: courseColor + .withAlpha(26), + borderRadius: + BorderRadius.circular( + 8, + ), + ), + child: Text( + '${course.units ?? "-"}' + ' ${context.ap.units}', + style: TextStyle( + fontSize: 12, + fontWeight: + FontWeight.w600, + color: courseColor, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_table_view.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_table_view.dart new file mode 100644 index 00000000..92f64212 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_table_view.dart @@ -0,0 +1,486 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; + +/// iOS-style pastel colors for course cells. +/// +/// These are softer, semi-transparent tones inspired by +/// Apple Calendar and iOS 26 Liquid Glass aesthetics, +/// designed to let the glass backdrop show through. +const List glassCourseColors = [ + Color(0xCC007AFF), // iOS Blue + Color(0xCC34C759), // iOS Green + Color(0xCCFF9500), // iOS Orange + Color(0xCCAF52DE), // iOS Purple + Color(0xCCFF3B30), // iOS Red + Color(0xCC5AC8FA), // iOS Light Blue + Color(0xCCFF2D55), // iOS Pink + Color(0xCC30D158), // iOS Mint Green + Color(0xCCFFCC00), // iOS Yellow + Color(0xCC5856D6), // iOS Indigo + Color(0xCC00C7BE), // iOS Teal + Color(0xFFA2845E), // iOS Brown +]; + +/// A glass-enhanced version of [CourseTableView]. +/// +/// Uses iOS-style pastel colors with translucent glass +/// backgrounds, rounded corners, and subtle frosted +/// effects for a Liquid Glass aesthetic. +class GlassCourseTableView extends StatelessWidget { + /// Creates a [GlassCourseTableView]. + const GlassCourseTableView({ + super.key, + required this.courseData, + required this.invisibleCourseCodes, + this.controller, + this.onCoursePressed, + required this.courseColorResolver, + this.repaintBoundaryKey, + this.mergeCourse, + this.showSectionTime, + this.showInstructors, + this.showClassroomLocation, + }); + + /// The course data to display. + final CourseData courseData; + + /// Course codes that should be hidden in the grid. + final List invisibleCourseCodes; + + /// Optional scroll controller for the grid. + final ScrollController? controller; + + /// Callback when a course cell is tapped. + final void Function( + Course course, + TimeCode timeCode, + int weekday, + )? onCoursePressed; + + /// Resolves a course code to its display color. + final Color Function(String courseCode) + courseColorResolver; + + /// Key for the [RepaintBoundary] used for screenshot + /// capture. + final GlobalKey? repaintBoundaryKey; + + /// Whether to merge adjacent identical course cells. + final bool? mergeCourse; + + /// Whether to show time information in time slot cells. + final bool? showSectionTime; + + /// Whether to show instructor names on course cards. + final bool? showInstructors; + + /// Whether to show classroom location on course cards. + final bool? showClassroomLocation; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final int weekdayCount = + courseData.hasHoliday ? 7 : 5; + final Map> courseLookup = + buildCourseLookup(courseData.courses); + return SingleChildScrollView( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.only( + bottom: 80.0, + ), + child: RepaintBoundary( + key: repaintBoundaryKey, + child: Column( + children: [ + _buildWeekdayHeader( + context, + colorScheme, + weekdayCount, + ), + _buildCourseGrid( + context, + colorScheme, + weekdayCount, + courseData.timeCodes, + courseLookup, + ), + ], + ), + ), + ); + } + + Widget _buildWeekdayHeader( + BuildContext context, + ColorScheme colorScheme, + int weekdayCount, + ) { + return Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: colorScheme.outlineVariant + .withAlpha(128), + ), + ), + ), + child: Row( + children: [ + _buildTimeSlotHeader(colorScheme), + for (int i = 0; i < weekdayCount; i++) + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + child: Text( + context.ap.weekdaysCourse[i], + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildTimeSlotHeader(ColorScheme colorScheme) { + return SizedBox( + width: 48, + height: 40, + ); + } + + Widget _buildCourseGrid( + BuildContext context, + ColorScheme colorScheme, + int weekdayCount, + List timeCodes, + Map> courseLookup, + ) { + final int minIndex = courseData.minTimeCodeIndex; + final int maxIndex = courseData.maxTimeCodeIndex; + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTimeColumn( + colorScheme, + timeCodes, + minIndex, + maxIndex, + ), + for (int weekday = 1; + weekday <= weekdayCount; + weekday++) + Expanded( + child: _buildWeekdayColumn( + colorScheme, + weekday, + weekdayCount, + minIndex, + maxIndex, + courseLookup, + ), + ), + ], + ); + } + + Widget _buildTimeColumn( + ColorScheme colorScheme, + List timeCodes, + int minIndex, + int maxIndex, + ) { + return Column( + children: [ + for (int i = minIndex; i <= maxIndex; i++) + _buildTimeSlot( + colorScheme, + i < timeCodes.length ? timeCodes[i] : null, + i, + i == maxIndex, + ), + ], + ); + } + + Widget _buildWeekdayColumn( + ColorScheme colorScheme, + int weekday, + int weekdayCount, + int minIndex, + int maxIndex, + Map> courseLookup, + ) { + final List children = []; + for (int i = minIndex; i <= maxIndex; i++) { + final Course? course = + courseLookup[weekday]?[i]; + final bool isInvisible = course != null && + invisibleCourseCodes.contains(course.code); + + if (course == null || isInvisible) { + children.add( + _buildEmptyCell( + colorScheme, + weekday < weekdayCount, + i == maxIndex, + ), + ); + } else { + int span = 1; + if (mergeCourse ?? true) { + while (i + span <= maxIndex && + courseLookup[weekday]?[i + span] == + course) { + span++; + } + } + children.add( + _buildCourseCell( + colorScheme, + weekday, + i, + course, + span, + weekday < weekdayCount, + i + span - 1 == maxIndex, + ), + ); + i += span - 1; + } + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, + ); + } + + Widget _buildTimeSlot( + ColorScheme colorScheme, + TimeCode? timeCode, + int timeIndex, + bool isLast, + ) { + return Container( + width: 48, + height: courseHeight, + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 4, + ), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest + .withAlpha(26), + border: Border( + right: BorderSide( + color: colorScheme.outlineVariant + .withAlpha(128), + ), + bottom: isLast + ? BorderSide.none + : BorderSide( + color: colorScheme.outlineVariant + .withAlpha(77), + width: 0.5, + ), + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + timeCode?.title ?? '${timeIndex + 1}', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: colorScheme.onSurfaceVariant, + ), + ), + if (timeCode != null && + (showSectionTime ?? true)) ...[ + const SizedBox(height: 2), + Text( + timeCode.startTime, + style: TextStyle( + fontSize: 9, + color: colorScheme.onSurfaceVariant + .withAlpha(179), + ), + ), + ], + ], + ), + ); + } + + Widget _buildEmptyCell( + ColorScheme colorScheme, + bool showRightBorder, + bool isLast, + ) { + return Container( + height: courseHeight, + decoration: BoxDecoration( + border: Border( + right: showRightBorder + ? BorderSide( + color: colorScheme.outlineVariant + .withAlpha(51), + width: 0.5, + ) + : BorderSide.none, + bottom: isLast + ? BorderSide.none + : BorderSide( + color: colorScheme.outlineVariant + .withAlpha(77), + width: 0.5, + ), + ), + ), + ); + } + + Widget _buildCourseCell( + ColorScheme colorScheme, + int weekday, + int timeIndex, + Course course, + int span, + bool showRightBorder, + bool isLast, + ) { + return Container( + height: courseHeight * span, + decoration: BoxDecoration( + border: Border( + right: showRightBorder + ? BorderSide( + color: colorScheme.outlineVariant + .withAlpha(51), + width: 0.5, + ) + : BorderSide.none, + bottom: isLast + ? BorderSide.none + : BorderSide( + color: colorScheme.outlineVariant + .withAlpha(77), + width: 0.5, + ), + ), + ), + child: _buildCourseCard( + colorScheme, + weekday, + timeIndex, + course, + span, + ), + ); + } + + Widget _buildCourseCard( + ColorScheme colorScheme, + int weekday, + int timeIndex, + Course course, + int span, + ) { + final Color courseColor = + courseColorResolver(course.code); + const Color onCourseColor = Colors.white; + final String locationInfo = + (showClassroomLocation ?? true) && + course.location != null + ? course.location.toString() + : ''; + final String instructorInfo = + (showInstructors ?? true) + ? course.getInstructors() + : ''; + + final String displayInfo = [ + if (instructorInfo.isNotEmpty) instructorInfo, + if (locationInfo.isNotEmpty) locationInfo, + ].join('\n'); + + return GestureDetector( + onTap: () { + final TimeCode timeCode = + timeIndex < courseData.timeCodes.length + ? courseData.timeCodes[timeIndex] + : const TimeCode( + title: '?', + startTime: '?', + endTime: '?', + ); + onCoursePressed?.call( + course, + timeCode, + weekday, + ); + }, + child: Container( + width: double.infinity, + height: double.infinity, + margin: const EdgeInsets.all(1.5), + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 4, + ), + decoration: BoxDecoration( + color: courseColor.withAlpha(179), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: onCourseColor.withAlpha(51), + width: 0.5, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + course.title, + textAlign: TextAlign.center, + maxLines: span > 1 ? 4 : 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: span > 1 ? 13 : 9.5, + fontWeight: FontWeight.w600, + color: onCourseColor, + height: 1.1, + ), + ), + if (displayInfo.isNotEmpty) ...[ + SizedBox(height: span > 1 ? 3 : 1), + Text( + displayInfo, + textAlign: TextAlign.center, + maxLines: span > 1 ? 4 : 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: span > 1 ? 11 : 8, + color: onCourseColor.withAlpha(204), + height: 1.0, + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_dialog.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_dialog.dart new file mode 100644 index 00000000..23fb6920 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_dialog.dart @@ -0,0 +1,76 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// Shows a [GlassDialog] with a single action button. +/// +/// This is the glass-enhanced equivalent of [DefaultDialog]. +Future showGlassDefaultDialog({ + required BuildContext context, + required String title, + required Widget contentWidget, + required String actionText, + required VoidCallback actionFunction, +}) { + return GlassDialog.show( + context: context, + title: title, + content: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: + MediaQuery.of(context).size.height * 0.5, + ), + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 8.0, + ), + child: contentWidget, + ), + ), + actions: [ + GlassDialogAction( + label: actionText, + onPressed: actionFunction, + ), + ], + ); +} + +/// Shows a [GlassDialog] with two action buttons (yes/no). +/// +/// This is the glass-enhanced equivalent of [YesNoDialog]. +Future showGlassYesNoDialog({ + required BuildContext context, + String? title, + Widget? contentWidget, + String? leftActionText, + String? rightActionText, + VoidCallback? leftActionFunction, + VoidCallback? rightActionFunction, +}) { + final ApLocalizations ap = context.ap; + return GlassDialog.show( + context: context, + title: title, + content: contentWidget, + actions: [ + GlassDialogAction( + label: leftActionText ?? ap.confirm, + onPressed: () { + Navigator.of(context, rootNavigator: true) + .pop(); + leftActionFunction?.call(); + }, + ), + GlassDialogAction( + label: rightActionText ?? ap.cancel, + onPressed: () { + Navigator.of(context, rootNavigator: true) + .pop(); + rightActionFunction?.call(); + }, + ), + ], + ); +} diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_floating_toolbar.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_floating_toolbar.dart new file mode 100644 index 00000000..8120c03a --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_floating_toolbar.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A floating toolbar inspired by Apple iOS 26 Liquid Glass. +/// +/// Renders two capsule-shaped glass groups (leading and +/// trailing) that float below the status bar, overlaying +/// content. Use inside a [Stack]. +/// +/// Widgets placed in [leading] and [trailing] should **not** +/// have their own glass layer — the toolbar capsule provides +/// the glass effect. Use plain [IconButton] or [Text]. +class GlassFloatingToolbar extends StatelessWidget { + const GlassFloatingToolbar({ + super.key, + this.leading = const [], + this.trailing = const [], + }); + + /// Widgets on the left capsule. + final List leading; + + /// Widgets on the right capsule. + final List trailing; + + @override + Widget build(BuildContext context) { + final double topPadding = + MediaQuery.of(context).padding.top; + + return Positioned( + top: topPadding + 6, + left: 16, + right: 16, + child: Row( + children: [ + if (leading.isNotEmpty) + GlassCard( + useOwnLayer: true, + height: 38, + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + shape: + const LiquidRoundedSuperellipse( + borderRadius: 19, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: leading, + ), + ), + const Spacer(), + if (trailing.isNotEmpty) + GlassCard( + useOwnLayer: true, + height: 38, + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + shape: + const LiquidRoundedSuperellipse( + borderRadius: 19, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: trailing, + ), + ), + ], + ), + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_cards.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_cards.dart new file mode 100644 index 00000000..fa8c8f80 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_cards.dart @@ -0,0 +1,615 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// Glass version of [ScorePRCard]. +class GlassScorePRCard extends StatelessWidget { + const GlassScorePRCard({ + super.key, + required this.analysis, + }); + + final ScoreAnalysis analysis; + + Color _getPRColor(ColorScheme colorScheme, int pr) { + if (pr >= 90) return const Color(0xFF34C759); + if (pr >= 75) return const Color(0xFF00C7BE); + if (pr >= 50) return const Color(0xFF007AFF); + if (pr >= 25) return const Color(0xFFFF9500); + return const Color(0xFFFF3B30); + } + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final int pr = analysis.estimatedPR; + final Color prColor = _getPRColor(colorScheme, pr); + + return GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: prColor.withAlpha(26), + borderRadius: + BorderRadius.circular(16), + ), + child: Icon( + Icons.trending_up_rounded, + size: 28, + color: prColor, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + '\u4f30\u8a08 PR \u503c', + style: TextStyle( + fontSize: 14, + color: colorScheme + .onSurfaceVariant, + ), + ), + const SizedBox(height: 4), + Text( + 'PR $pr', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: prColor, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: prColor.withAlpha(26), + borderRadius: + BorderRadius.circular(20), + ), + child: Text( + analysis.prLevel, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: prColor, + ), + ), + ), + ], + ), + ), + Container( + margin: + const EdgeInsets.fromLTRB(16, 0, 16, 16), + height: 8, + decoration: BoxDecoration( + color: + colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(4), + ), + child: FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: pr / 100, + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + prColor.withAlpha(179), + prColor, + ], + ), + borderRadius: + BorderRadius.circular(4), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB( + 16, + 0, + 16, + 16, + ), + child: Text( + '※ PR 值為根據平均成績估算,僅供參考', + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant + .withAlpha(179), + ), + ), + ), + ], + ), + ); + } +} + +/// Glass version of [ScoreStatisticsCard]. +class GlassScoreStatisticsCard extends StatelessWidget { + const GlassScoreStatisticsCard({ + super.key, + required this.analysis, + }); + + final ScoreAnalysis analysis; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon( + Icons.bar_chart_rounded, + size: 20, + color: colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + '\u6210\u7e3e\u7d71\u8a08', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + ], + ), + ), + const GlassDivider(), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: _GlassScoreStatItem( + label: '\u6700\u9ad8\u5206', + value: analysis.maxScore + .toStringAsFixed(0), + icon: + Icons.arrow_upward_rounded, + color: const Color(0xFF34C759), + ), + ), + const SizedBox(width: 12), + Expanded( + child: _GlassScoreStatItem( + label: '\u6700\u4f4e\u5206', + value: analysis.minScore + .toStringAsFixed(0), + icon: Icons + .arrow_downward_rounded, + color: colorScheme.error, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _GlassScoreStatItem( + label: '\u6a19\u6e96\u5dee', + value: analysis.standardDeviation + .toStringAsFixed(2), + icon: + Icons.analytics_outlined, + color: colorScheme.tertiary, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _GlassScoreStatItem( + label: '\u79d1\u76ee\u6578', + value: analysis.totalSubjects + .toString(), + icon: + Icons.menu_book_rounded, + color: colorScheme.secondary, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} + +class _GlassScoreStatItem extends StatelessWidget { + const _GlassScoreStatItem({ + required this.label, + required this.value, + required this.icon, + required this.color, + }); + + final String label; + final String value; + final IconData icon; + final Color color; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: color.withAlpha(13), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: color.withAlpha(26), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, size: 18, color: color), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12, + color: + colorScheme.onSurfaceVariant, + ), + ), + Text( + value, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +/// Glass version of [ScoreDistributionCard]. +class GlassScoreDistributionCard + extends StatelessWidget { + const GlassScoreDistributionCard({ + super.key, + required this.analysis, + }); + + final ScoreAnalysis analysis; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon( + Icons.pie_chart_rounded, + size: 20, + color: colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + '\u6210\u7e3e\u5206\u4f48', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + ], + ), + ), + const GlassDivider(), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _GlassDistributionBar( + label: + '90-100 (\u512a\u79c0)', + count: analysis + .distribution['90-100'] ?? + 0, + total: analysis.totalSubjects, + color: const Color(0xFF34C759), + ), + const SizedBox(height: 10), + _GlassDistributionBar( + label: + '80-89 (\u826f\u597d)', + count: analysis + .distribution['80-89'] ?? + 0, + total: analysis.totalSubjects, + color: const Color(0xFF00C7BE), + ), + const SizedBox(height: 10), + _GlassDistributionBar( + label: + '70-79 (\u666e\u901a)', + count: analysis + .distribution['70-79'] ?? + 0, + total: analysis.totalSubjects, + color: const Color(0xFF007AFF), + ), + const SizedBox(height: 10), + _GlassDistributionBar( + label: + '60-69 (\u53ca\u683c)', + count: analysis + .distribution['60-69'] ?? + 0, + total: analysis.totalSubjects, + color: const Color(0xFFFF9500), + ), + const SizedBox(height: 10), + _GlassDistributionBar( + label: '0-59 ' + '(\u4e0d\u53ca\u683c)', + count: analysis + .distribution['0-59'] ?? + 0, + total: analysis.totalSubjects, + color: const Color(0xFFFF3B30), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _GlassDistributionBar extends StatelessWidget { + const _GlassDistributionBar({ + required this.label, + required this.count, + required this.total, + required this.color, + }); + + final String label; + final int count; + final int total; + final Color color; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final double percentage = + total > 0 ? count / total : 0; + + return Row( + children: [ + SizedBox( + width: 100, + child: Text( + label, + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + ), + Expanded( + child: Container( + height: 20, + decoration: BoxDecoration( + color: + colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(10), + ), + child: FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: percentage, + child: DecoratedBox( + decoration: BoxDecoration( + color: color, + borderRadius: + BorderRadius.circular(10), + ), + ), + ), + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 40, + child: Text( + '$count \u79d1', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: colorScheme.onSurface, + ), + textAlign: TextAlign.end, + ), + ), + ], + ); + } +} + +/// Glass version of [ScoreCreditSummaryCard]. +class GlassScoreCreditSummaryCard + extends StatelessWidget { + const GlassScoreCreditSummaryCard({ + super.key, + required this.analysis, + }); + + final ScoreAnalysis analysis; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon( + Icons.account_balance_rounded, + size: 20, + color: colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + '\u5b78\u5206\u7d71\u8a08', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + ], + ), + ), + const GlassDivider(), + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: _GlassCreditItem( + label: '\u4fee\u7fd2\u5b78\u5206', + value: analysis.totalCredits + .toStringAsFixed(1), + color: colorScheme.primary, + ), + ), + Expanded( + child: _GlassCreditItem( + label: '\u53ca\u683c\u5b78\u5206', + value: analysis.passedCredits + .toStringAsFixed(1), + color: const Color(0xFF34C759), + ), + ), + Expanded( + child: _GlassCreditItem( + label: + '\u4e0d\u53ca\u683c\u5b78' + '\u5206', + value: analysis.failedCredits + .toStringAsFixed(1), + color: colorScheme.error, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _GlassCreditItem extends StatelessWidget { + const _GlassCreditItem({ + required this.label, + required this.value, + required this.color, + }); + + final String label; + final String value; + final Color color; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return Column( + children: [ + Text( + value, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: color, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_tab.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_tab.dart new file mode 100644 index 00000000..2636d7be --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_tab.dart @@ -0,0 +1,215 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_score_analysis_cards.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// Glass version of [ScoreAnalysisTab]. +/// +/// Replaces the summary card's gradient [Container] with +/// [GlassCard] and uses glass analysis card variants. +class GlassScoreAnalysisTab extends StatelessWidget { + const GlassScoreAnalysisTab({ + super.key, + required this.scoreData, + this.onRefresh, + this.controller, + }); + + final ScoreData scoreData; + final VoidCallback? onRefresh; + final ScrollController? controller; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final ScoreAnalysis analysis = + ScoreAnalysis(scoreData); + + return RefreshIndicator( + onRefresh: () async => onRefresh?.call(), + child: SingleChildScrollView( + controller: controller, + physics: + const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: 80, + ), + child: Column( + children: [ + _buildMainSummaryCard( + colorScheme, + context.ap, + analysis, + ), + const SizedBox(height: 16), + GlassScorePRCard(analysis: analysis), + const SizedBox(height: 16), + GlassScoreStatisticsCard( + analysis: analysis, + ), + const SizedBox(height: 16), + GlassScoreDistributionCard( + analysis: analysis, + ), + const SizedBox(height: 16), + GlassScoreCreditSummaryCard( + analysis: analysis, + ), + const SizedBox(height: 32), + ], + ), + ), + ); + } + + Widget _buildMainSummaryCard( + ColorScheme colorScheme, + ApLocalizations ap, + ScoreAnalysis analysis, + ) { + final Detail detail = scoreData.detail; + + return GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Expanded( + child: _buildMainItem( + colorScheme, + Icons.star_rounded, + ap.average, + detail.average + ?.toStringAsFixed(2) ?? + '-', + ), + ), + Container( + width: 1, + height: 60, + color: colorScheme + .onPrimaryContainer + .withAlpha(51), + ), + Expanded( + child: _buildMainItem( + colorScheme, + Icons.school_rounded, + ap.conductScore, + detail.conduct + ?.toStringAsFixed(0) ?? + '-', + ), + ), + ], + ), + ), + GlassContainer( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + shape: const LiquidRoundedSuperellipse( + borderRadius: 20, + ), + child: Row( + children: [ + Expanded( + child: _buildRankItem( + colorScheme, + ap.classRank, + detail.classRank ?? '-', + ), + ), + Container( + width: 1, + height: 40, + color: colorScheme.outlineVariant + .withAlpha(128), + ), + Expanded( + child: _buildRankItem( + colorScheme, + ap.departmentRank, + detail.departmentRank ?? '-', + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildMainItem( + ColorScheme colorScheme, + IconData icon, + String label, + String value, + ) { + return Column( + children: [ + Icon( + icon, + size: 28, + color: colorScheme.primary, + ), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: + colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + fontSize: 13, + color: colorScheme.onPrimaryContainer + .withAlpha(179), + ), + ), + ], + ); + } + + Widget _buildRankItem( + ColorScheme colorScheme, + String label, + String value, + ) { + return Column( + children: [ + Text( + value, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 2), + Text( + label, + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_list_tab.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_list_tab.dart new file mode 100644 index 00000000..61200a9c --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_list_tab.dart @@ -0,0 +1,210 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// Glass version of [ScoreListTab]. +/// +/// Replaces each score item's [Container] with [GlassCard]. +class GlassScoreListTab extends StatelessWidget { + const GlassScoreListTab({ + super.key, + required this.scoreData, + this.onRefresh, + this.middleTitle, + this.finalTitle, + this.onScoreSelect, + this.middleScoreBuilder, + this.finalScoreBuilder, + this.controller, + }); + + final ScoreData scoreData; + final VoidCallback? onRefresh; + final String? middleTitle; + final String? finalTitle; + final Function(int index)? onScoreSelect; + final Widget Function(int index)? middleScoreBuilder; + final Widget Function(int index)? finalScoreBuilder; + final ScrollController? controller; + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: () async => onRefresh?.call(), + child: ListView.builder( + controller: controller, + physics: + const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: 80, + ), + itemCount: scoreData.scores.length, + itemBuilder: + (BuildContext context, int index) { + return _buildScoreItem( + Theme.of(context).colorScheme, + scoreData.scores[index], + index, + ); + }, + ), + ); + } + + Widget _buildScoreItem( + ColorScheme colorScheme, + Score score, + int index, + ) { + final String scoreStr = + score.semesterScore ?? ''; + final double? scoreValue = + double.tryParse(scoreStr); + final bool isPassed = + scoreValue != null && scoreValue >= 60; + final Color scoreColor = scoreValue == null + ? colorScheme.onSurfaceVariant + : isPassed + ? _getScoreColor(scoreValue) + : colorScheme.error; + + return GestureDetector( + onTap: onScoreSelect == null + ? null + : () { + onScoreSelect!(index); + AnalyticsUtil.instance + .logEvent('score_title_click'); + }, + child: Padding( + padding: const EdgeInsets.only(bottom: 8), + child: GlassCard( + useOwnLayer: true, + child: Row( + children: [ + Container( + width: 4, + height: 50, + decoration: BoxDecoration( + color: scoreColor, + borderRadius: + BorderRadius.circular(2), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + score.title, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: + colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + _buildTag( + colorScheme, + score.required ?? '', + colorScheme.tertiary, + ), + const SizedBox(width: 8), + Text( + '${score.units}' + ' \u5b78\u5206', + style: TextStyle( + fontSize: 12, + color: colorScheme + .onSurfaceVariant, + ), + ), + ], + ), + ], + ), + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + if (finalScoreBuilder == null) + Text( + score.semesterScore ?? '-', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: scoreColor, + ), + ), + if (finalScoreBuilder != null) + finalScoreBuilder!(index), + const SizedBox(height: 4), + if (middleScoreBuilder == null && + score.middleScore != null && + score.middleScore!.isNotEmpty) + Text( + '\u671f\u4e2d: ' + '${score.middleScore}', + style: TextStyle( + fontSize: 11, + color: colorScheme + .onSurfaceVariant, + ), + ), + if (middleScoreBuilder != null) + middleScoreBuilder!(index), + ], + ), + ], + ), + ), + ), + ); + } + + Color _getScoreColor(double score) { + if (score >= 90) return const Color(0xFF34C759); + if (score >= 80) return const Color(0xFF00C7BE); + if (score >= 70) return const Color(0xFF007AFF); + if (score >= 60) return const Color(0xFFFF9500); + return const Color(0xFFFF3B30); + } + + Widget _buildTag( + ColorScheme colorScheme, + String text, + Color color, + ) { + if (text.isEmpty) return const SizedBox.shrink(); + + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: color.withAlpha(26), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + text + .replaceAll('\u3010', '') + .replaceAll('\u3011', ''), + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: color, + ), + ), + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_semester_picker.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_semester_picker.dart new file mode 100644 index 00000000..8ad61ec6 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_semester_picker.dart @@ -0,0 +1,910 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [SemesterPicker]. +/// +/// Replaces Material widgets with Liquid Glass equivalents while +/// maintaining the same API surface. The inline chip button uses +/// [GlassCard] and [Icons], and the bottom sheet uses +/// [GlassSheet] with glass-styled semester items. +class GlassSemesterPicker extends StatefulWidget { + const GlassSemesterPicker({ + super.key, + required this.semesterData, + this.onSelect, + this.featureTag, + this.currentIndex = 0, + this.uiConfig, + this.controller, + }); + + final SemesterData semesterData; + final SemesterCallback? onSelect; + final String? featureTag; + final int currentIndex; + final SemesterUIConfig? uiConfig; + final SemesterPickerController? controller; + + @override + GlassSemesterPickerState createState() => + GlassSemesterPickerState(); + + /// Shows a glass-styled semester picker bottom sheet. + static void show({ + required BuildContext context, + required SemesterData semesterData, + int currentIndex = 0, + SemesterCallback? onSelect, + String? featureTag, + Set? loadingSemesters, + Set? emptySemesters, + SemesterUIConfig? uiConfig, + SemesterPickerController? controller, + }) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final List> sortedSemesters = + _getSortedSemesters(semesterData, uiConfig); + + final Map>> + groupedByYear = + >>{}; + for (final MapEntry entry + in sortedSemesters) { + final String year = entry.value.year; + groupedByYear.putIfAbsent( + year, + () => >[], + ); + groupedByYear[year]!.add(entry); + } + + // Prefer controller's sets; fall back to explicit + // parameters. + final Set? effectiveLoading = + controller?.loadingSemesters ?? loadingSemesters; + final Set? effectiveEmpty = + controller?.emptySemesters ?? emptySemesters; + + VoidCallback? onControllerChanged; + GlassSheet.show( + context: context, + isScrollControlled: true, + builder: (BuildContext sheetContext) { + if (controller != null) { + controller.attachSheetNavigator( + Navigator.of(sheetContext), + ); + } + return StatefulBuilder( + builder: ( + BuildContext context, + StateSetter setSheetState, + ) { + if (onControllerChanged == null && + controller != null) { + onControllerChanged = + () => setSheetState(() {}); + controller + .addListener(onControllerChanged!); + } + return SizedBox( + height: MediaQuery.of(context) + .size + .height * + 0.6, + child: Column( + children: [ + Padding( + padding: + const EdgeInsets.all(20), + child: Row( + children: [ + Icon( + Icons.calendar_month_rounded, + color: + colorScheme.primary, + ), + const SizedBox(width: 12), + Text( + context.ap.pickSemester, + style: TextStyle( + fontSize: 20, + fontWeight: + FontWeight.bold, + color: colorScheme + .onSurface, + ), + ), + ], + ), + ), + const GlassDivider(), + Expanded( + child: ListView.builder( + padding: + const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + itemCount: + groupedByYear.length, + itemBuilder: ( + BuildContext context, + int groupIndex, + ) { + final String year = + groupedByYear.keys + .elementAt( + groupIndex, + ); + final List< + MapEntry> + semesters = + groupedByYear[year]!; + + return _buildYearGroup( + context: context, + year: year, + semesters: semesters, + colorScheme: + colorScheme, + currentIndex: + currentIndex, + semesterData: + semesterData, + onSelect: onSelect, + loadingSemesters: + effectiveLoading, + emptySemesters: + effectiveEmpty, + setSheetState: + setSheetState, + uiConfig: uiConfig, + controller: controller, + ); + }, + ), + ), + ], + ), + ); + }, + ); + }, + ).whenComplete(() { + if (onControllerChanged != null) { + controller + ?.removeListener(onControllerChanged!); + } + controller?.detachSheetNavigator(); + }); + } + + static Widget _buildYearGroup({ + required BuildContext context, + required String year, + required List> semesters, + required ColorScheme colorScheme, + required int currentIndex, + required SemesterData semesterData, + SemesterCallback? onSelect, + Set? loadingSemesters, + Set? emptySemesters, + required StateSetter setSheetState, + SemesterUIConfig? uiConfig, + SemesterPickerController? controller, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: + const EdgeInsets.fromLTRB(8, 16, 8, 8), + child: Row( + children: [ + GlassCard( + useOwnLayer: true, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + shape: + const LiquidRoundedSuperellipse( + borderRadius: 8, + ), + child: Text( + context.ap + .schoolYearFormat(arg1: year), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: + colorScheme.onPrimaryContainer, + ), + ), + ), + const Expanded(child: SizedBox()), + ], + ), + ), + ...semesters.map( + (MapEntry entry) => + _buildSemesterItem( + context: context, + originalIndex: entry.key, + semester: entry.value, + colorScheme: colorScheme, + currentIndex: currentIndex, + semesterData: semesterData, + onSelect: onSelect, + loadingSemesters: loadingSemesters, + emptySemesters: emptySemesters, + setSheetState: setSheetState, + uiConfig: uiConfig, + controller: controller, + ), + ), + ], + ); + } + + static Widget _buildSemesterItem({ + required BuildContext context, + required int originalIndex, + required Semester semester, + required ColorScheme colorScheme, + required int currentIndex, + required SemesterData semesterData, + SemesterCallback? onSelect, + Set? loadingSemesters, + Set? emptySemesters, + required StateSetter setSheetState, + SemesterUIConfig? uiConfig, + SemesterPickerController? controller, + }) { + final bool isSelected = + originalIndex == currentIndex; + final bool isDefault = + originalIndex == semesterData.defaultIndex; + final bool isEmpty = + emptySemesters?.contains(semester.code) ?? + false; + final bool isLoading = + loadingSemesters?.contains(semester.code) ?? + false; + final bool isDisabled = isEmpty || isLoading; + final String semesterName = + uiConfig?.getName?.call(semester.value) ?? + _getSemesterName( + semester.value, + context.ap, + ); + final String displayName = semesterName.isNotEmpty + ? semesterName + : semester.text; + + return Opacity( + opacity: isEmpty ? 0.5 : 1.0, + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 4), + child: GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: GestureDetector( + onTap: isDisabled + ? null + : () { + if (onSelect != null) { + if (controller != null) { + controller + .markSemesterLoading( + semester, + ); + onSelect( + semester, + originalIndex, + ); + } else if (loadingSemesters != + null) { + loadingSemesters + .add(semester.code); + setSheetState(() {}); + onSelect( + semester, + originalIndex, + ); + } else { + Navigator.of(context).pop(); + onSelect( + semester, + originalIndex, + ); + } + } else { + Navigator.of(context).pop(); + } + }, + behavior: HitTestBehavior.opaque, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: isEmpty + ? colorScheme.outlineVariant + .withAlpha(77) + : isLoading + ? colorScheme.primary + .withAlpha(77) + : isSelected + ? colorScheme + .primary + : (uiConfig + ?.getColor + ?.call( + semester + .value, + colorScheme, + ) ?? + _getSemesterColor( + semester + .value, + colorScheme, + )), + borderRadius: + BorderRadius.circular(10), + ), + child: Center( + child: isLoading + ? const SizedBox( + width: 20, + height: 20, + child: + GlassProgressIndicator + .circular(), + ) + : Icon( + isEmpty + ? Icons + .block_rounded + : (uiConfig?.getIcon + ?.call( + semester + .value, + ) ?? + _getSemesterIcon( + semester + .value, + )), + size: 22, + color: isEmpty + ? colorScheme + .onSurfaceVariant + : isSelected + ? colorScheme + .onPrimary + : colorScheme + .onPrimaryContainer, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text( + displayName, + style: TextStyle( + fontSize: 16, + fontWeight: + FontWeight.w600, + color: isEmpty + ? colorScheme + .onSurfaceVariant + : isLoading + ? colorScheme + .primary + : isSelected + ? colorScheme + .primary + : colorScheme + .onSurface, + ), + overflow: TextOverflow + .ellipsis, + ), + ), + if (isLoading) ...[ + const SizedBox( + width: 8, + ), + Container( + padding: + const EdgeInsets + .symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: + BoxDecoration( + color: colorScheme + .primary + .withAlpha(26), + borderRadius: + BorderRadius + .circular( + 4, + ), + ), + child: Text( + context.ap + .semesterLoading, + style: TextStyle( + fontSize: 10, + fontWeight: + FontWeight + .w500, + color: + colorScheme + .primary, + ), + ), + ), + ] else if ( + isEmpty) ...[ + const SizedBox( + width: 8, + ), + Container( + padding: + const EdgeInsets + .symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: + BoxDecoration( + color: colorScheme + .outlineVariant + .withAlpha(77), + borderRadius: + BorderRadius + .circular( + 4, + ), + ), + child: Text( + context.ap + .semesterNoData, + style: TextStyle( + fontSize: 10, + fontWeight: + FontWeight + .w500, + color: colorScheme + .onSurfaceVariant, + ), + ), + ), + ] else if ( + isDefault) ...[ + const SizedBox( + width: 8, + ), + Container( + padding: + const EdgeInsets + .symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: + BoxDecoration( + color: colorScheme + .tertiary, + borderRadius: + BorderRadius + .circular( + 4, + ), + ), + child: Text( + context.ap + .currentSemester, + style: TextStyle( + fontSize: 10, + fontWeight: + FontWeight + .w600, + color: colorScheme + .onTertiary, + ), + ), + ), + ], + ], + ), + const SizedBox(height: 2), + Text( + semester.text, + style: TextStyle( + fontSize: 12, + color: colorScheme + .onSurfaceVariant, + ), + ), + ], + ), + ), + if (isLoading) + const SizedBox( + width: 24, + height: 24, + child: GlassProgressIndicator + .circular(), + ) + else if (isEmpty) + Icon( + Icons.lock_outline_rounded, + color: + colorScheme.outlineVariant, + size: 20, + ) + else if (isSelected) + Icon( + Icons + .check_circle_rounded, + color: colorScheme.primary, + ) + else + Icon( + Icons.circle_outlined, + color: + colorScheme.outlineVariant, + ), + ], + ), + ), + ), + ), + ), + ); + } + + // ---------------------------------------------------------- + // Duplicated helpers (private in SemesterPickerState) + // ---------------------------------------------------------- + + static int _getSemesterSortValue(String value) { + switch (value) { + case '4': + return 1; + case '6': + return 2; + case '7': + return 3; + case '2': + return 4; + case '3': + return 5; + case '1': + return 6; + case '5': + return 7; + default: + return 99; + } + } + + static String _getSemesterName( + String value, [ + ApLocalizations? ap, + ]) { + switch (value) { + case '1': + return ap?.firstSemester ?? '上學期'; + case '2': + return ap?.secondSemester ?? '下學期'; + case '3': + return ap?.winterSession ?? '寒修'; + case '4': + return ap?.summerSession ?? '暑修'; + case '5': + return ap?.preCourse ?? '先修'; + case '6': + return ap?.summerSessionFirst ?? '暑修(一)'; + case '7': + return ap?.summerSessionSpecial ?? '暑修(特)'; + default: + return ''; + } + } + + static IconData _getSemesterIcon(String value) { + switch (value) { + case '1': + return Icons.looks_one_rounded; + case '2': + return Icons.looks_one_rounded; + case '3': + return Icons.ac_unit_rounded; + case '4': + case '6': + case '7': + return Icons.brightness_6; + case '5': + return Icons.auto_awesome_rounded; + default: + return Icons.calendar_month_rounded; + } + } + + static Color _getSemesterColor( + String value, + ColorScheme colorScheme, + ) { + switch (value) { + case '1': + return colorScheme.primaryContainer + .withAlpha(179); + case '2': + return colorScheme.secondaryContainer + .withAlpha(179); + case '3': + return colorScheme.errorContainer + .withAlpha(128); + case '5': + return colorScheme.primaryContainer + .withAlpha(102); + case '4': + case '6': + case '7': + return colorScheme.tertiaryContainer + .withAlpha(179); + default: + return colorScheme.surfaceContainerHighest; + } + } + + static List> + _getSortedSemesters( + SemesterData semesterData, + SemesterUIConfig? uiConfig, + ) { + final List> indexed = + >[]; + for (int i = 0; + i < semesterData.data.length; + i++) { + indexed.add( + MapEntry( + i, + semesterData.data[i], + ), + ); + } + + indexed.sort( + ( + MapEntry a, + MapEntry b, + ) { + final int yearA = + int.tryParse(a.value.year) ?? 0; + final int yearB = + int.tryParse(b.value.year) ?? 0; + + if (yearA != yearB) { + return yearB.compareTo(yearA); + } + + final int semA = uiConfig?.getSortValue + ?.call(a.value.value) ?? + _getSemesterSortValue(a.value.value); + final int semB = uiConfig?.getSortValue + ?.call(b.value.value) ?? + _getSemesterSortValue(b.value.value); + return semA.compareTo(semB); + }, + ); + + return indexed; + } + + static String _getShortSemesterText( + Semester semester, + SemesterUIConfig? uiConfig, [ + ApLocalizations? ap, + ]) { + final String name = + uiConfig?.getName?.call(semester.value) ?? + _getSemesterName(semester.value, ap); + if (name.isNotEmpty) { + return '${semester.year} $name'; + } + return semester.text; + } +} + +/// State for [GlassSemesterPicker]. +class GlassSemesterPickerState + extends State { + late SemesterData semesterData; + Semester? selectSemester; + late int currentIndex; + + final Set _emptySemesters = {}; + final Set _loadingSemesters = {}; + BuildContext? _sheetContext; + StateSetter? _sheetSetState; + + @override + void initState() { + semesterData = widget.semesterData; + currentIndex = widget.currentIndex; + if (semesterData.data.isNotEmpty && + currentIndex < semesterData.data.length) { + selectSemester = + semesterData.data[currentIndex]; + } + super.initState(); + } + + @override + void didUpdateWidget( + covariant GlassSemesterPicker oldWidget, + ) { + if (oldWidget.semesterData != + widget.semesterData || + oldWidget.currentIndex != + widget.currentIndex) { + semesterData = widget.semesterData; + currentIndex = widget.currentIndex; + if (semesterData.data.isNotEmpty && + currentIndex < semesterData.data.length) { + selectSemester = + semesterData.data[currentIndex]; + } + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final String displayText = selectSemester != null + ? GlassSemesterPicker._getShortSemesterText( + selectSemester!, + widget.uiConfig, + context.ap, + ) + : ''; + + return GestureDetector( + onTap: () { + if (selectSemester != null) pickSemester(); + if (widget.featureTag != null) { + AnalyticsUtil.instance.logEvent( + '${widget.featureTag}_item_picker_click', + ); + } + }, + behavior: HitTestBehavior.opaque, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.calendar_month_rounded, + size: 16, + color: colorScheme.onSurface, + ), + const SizedBox(width: 6), + Text( + displayText, + style: TextStyle( + color: colorScheme.onSurface, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 2), + Icon( + Icons.arrow_drop_down, + size: 14, + color: colorScheme.onSurface, + ), + ], + ), + ); + } + + /// Mark a semester as empty (no data). + void markSemesterEmpty(Semester semester) { + _loadingSemesters.remove(semester.code); + _emptySemesters.add(semester.code); + _sheetSetState?.call(() {}); + if (mounted) setState(() {}); + } + + /// Mark a semester as having data. Auto-closes + /// the bottom sheet. + void markSemesterHasData(Semester semester) { + _loadingSemesters.remove(semester.code); + _emptySemesters.remove(semester.code); + if (_sheetContext != null && + Navigator.of(_sheetContext!).canPop()) { + Navigator.of(_sheetContext!).pop(); + _sheetContext = null; + _sheetSetState = null; + } + if (mounted) setState(() {}); + } + + /// Mark a semester as loading. + void markSemesterLoading(Semester semester) { + _loadingSemesters.add(semester.code); + _sheetSetState?.call(() {}); + if (mounted) setState(() {}); + } + + /// Whether the given semester is marked empty. + bool isSemesterEmpty(Semester semester) { + return _emptySemesters.contains(semester.code); + } + + /// Whether the given semester is currently loading. + bool isSemesterLoading(Semester semester) { + return _loadingSemesters.contains(semester.code); + } + + /// Opens the semester picker bottom sheet. + void pickSemester() { + final SemesterPickerController? ctrl = + widget.controller; + GlassSemesterPicker.show( + context: context, + semesterData: semesterData, + currentIndex: currentIndex, + onSelect: (Semester semester, int index) { + HapticFeedback.selectionClick(); + if (ctrl != null) { + ctrl.markSemesterLoading(semester); + } else { + markSemesterLoading(semester); + } + currentIndex = index; + selectSemester = + semesterData.data[currentIndex]; + widget.onSelect?.call( + semesterData.data[currentIndex], + currentIndex, + ); + if (mounted) setState(() {}); + }, + featureTag: widget.featureTag, + loadingSemesters: + ctrl != null ? null : _loadingSemesters, + emptySemesters: + ctrl != null ? null : _emptySemesters, + uiConfig: widget.uiConfig, + controller: ctrl, + ); + } +} diff --git a/packages/ap_common_liquid_glass/pubspec.yaml b/packages/ap_common_liquid_glass/pubspec.yaml new file mode 100644 index 00000000..dafb3b74 --- /dev/null +++ b/packages/ap_common_liquid_glass/pubspec.yaml @@ -0,0 +1,29 @@ +name: ap_common_liquid_glass +description: > + Liquid Glass design language for AP series apps. Provides glass-effect + scaffold variants and widgets powered by liquid_glass_widgets. +version: 0.1.0-dev.1 +homepage: https://github.com/abc873693/ap_common +repository: https://github.com/abc873693/ap_common/tree/master/packages/ap_common_liquid_glass + +environment: + sdk: ^3.9.0 + flutter: ">=3.24.0 <4.0.0" + +resolution: workspace + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + ap_common_flutter_ui: ^1.1.0-dev.6 + liquid_glass_widgets: ^0.7.0 + barcode_widget: ^2.0.1 + cached_network_image: ^3.2.3 + flutter_linkify: ^6.0.0 + photo_view: ^0.15.0 + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/pubspec.lock b/pubspec.lock index d33de1cc..f3f786ed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -393,6 +393,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.3" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" fake_async: dependency: transitive description: @@ -707,6 +715,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.34" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" flutter_test: dependency: transitive description: flutter @@ -1053,6 +1069,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + liquid_glass_widgets: + dependency: transitive + description: + name: liquid_glass_widgets + sha256: ac8ebf50943fb9dd4b71ea5e1945a3e77ea2015d87c27492703c45c7f8dc0cab + url: "https://pub.dev" + source: hosted + version: "0.7.0" logger: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 399c50d2..1e6b5c73 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,9 @@ workspace: - packages/ap_common_announcement_ui - packages/ap_common_plugin - packages/ap_common_firebase + - packages/ap_common_liquid_glass - apps/example + - apps/example_liquid_glass dev_dependencies: melos: ^7.5.0