Rebrand as TPSchedule and refine clear liquid glass dock

This commit is contained in:
Codex
2026-10-13 22:08:20 +08:00
parent 50fcc70e8e
commit 204e945b2e
45 changed files with 1155 additions and 103 deletions
+1
View File
@@ -6,6 +6,7 @@ gradle-wrapper.jar
/local.properties
GeneratedPluginRegistrant.java
.cxx/
.kotlin/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
+2 -3
View File
@@ -5,7 +5,7 @@ plugins {
}
android {
namespace = "com.example.hepan_everyday"
namespace = "com.tpschedule.app"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@@ -16,8 +16,7 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.hepan_everyday"
applicationId = "com.tpschedule.app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 24
@@ -6,10 +6,11 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application
android:label="RSchedule"
android:label="TPSchedule"
android:networkSecurityConfig="@xml/network_security_config"
android:name="${applicationName}"
android:icon="@drawable/ic_hepan_launcher">
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher">
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" android:exported="false" />
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver" android:exported="false">
<intent-filter>
@@ -1,4 +1,4 @@
package com.example.hepan_everyday
package com.tpschedule.app
import android.Manifest
import android.app.Activity
@@ -1,4 +1,4 @@
package com.example.hepan_everyday
package com.tpschedule.app
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
@@ -1,12 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<item android:drawable="@drawable/tpschedule_gradient" />
<item android:width="120dp" android:height="120dp" android:gravity="center" android:drawable="@drawable/ic_tpschedule_logo" />
</layer-list>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="108dp" android:height="108dp" android:viewportWidth="108" android:viewportHeight="108">
<group android:scaleX="0.035" android:scaleY="0.035" android:translateX="18.855275" android:translateY="18.83165">
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFD22A" android:strokeWidth="217" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M425.1,514.64 H1583.18 A20,20 0,0 1,1603.18 534.64 V1566.53 A20,20 0,0 1,1583.18 1586.53 H425.1 A20,20 0,0 1,405.1 1566.53 V534.64 A20,20 0,0 1,425.1 514.64 Z" />
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFFFFF" android:strokeWidth="150" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M731.72,339.92 H740.93 A27.65,27.65 0,0 1,768.58 367.57 V661.71 A27.65,27.65 0,0 1,740.93 689.36 H731.72 A27.65,27.65 0,0 1,704.07 661.71 V367.57 A27.65,27.65 0,0 1,731.72 339.92 Z M1277.56,339.92 H1286.77 A27.65,27.65 0,0 1,1314.42 367.57 V661.71 A27.65,27.65 0,0 1,1286.77 689.36 H1277.56 A27.65,27.65 0,0 1,1249.91 661.71 V367.57 A27.65,27.65 0,0 1,1277.56 339.92 Z" />
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFFFFF" android:strokeWidth="185" android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M729.01,1004.81 c70.21,112.08 200.6,174.7 327.98,152.69 c122.48,-18.51 206.2,-123.19 222.28,-152.69" />
</group>
</vector>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" android:width="108dp" android:height="108dp" android:viewportWidth="2008.27" android:viewportHeight="2009.62">
<path android:pathData="M0.5,0.5 H2007.77 V2009.12 H0.5 Z" android:strokeColor="#000000" android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient android:type="linear" android:startX="324.09" android:startY="1572.05" android:endX="1871.99" android:endY="280.93">
<item android:offset="0" android:color="#3FD1FF" />
<item android:offset="0.28" android:color="#38B3FF" />
<item android:offset="1" android:color="#2A6CFF" />
</gradient>
</aapt:attr>
</path>
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFD22A" android:strokeWidth="217" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M425.1,514.64 H1583.18 A20,20 0,0 1,1603.18 534.64 V1566.53 A20,20 0,0 1,1583.18 1586.53 H425.1 A20,20 0,0 1,405.1 1566.53 V534.64 A20,20 0,0 1,425.1 514.64 Z" />
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFFFFF" android:strokeWidth="150" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M731.72,339.92 H740.93 A27.65,27.65 0,0 1,768.58 367.57 V661.71 A27.65,27.65 0,0 1,740.93 689.36 H731.72 A27.65,27.65 0,0 1,704.07 661.71 V367.57 A27.65,27.65 0,0 1,731.72 339.92 Z M1277.56,339.92 H1286.77 A27.65,27.65 0,0 1,1314.42 367.57 V661.71 A27.65,27.65 0,0 1,1286.77 689.36 H1277.56 A27.65,27.65 0,0 1,1249.91 661.71 V367.57 A27.65,27.65 0,0 1,1277.56 339.92 Z" />
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFFFFF" android:strokeWidth="185" android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M729.01,1004.81 c70.21,112.08 200.6,174.7 327.98,152.69 c122.48,-18.51 206.2,-123.19 222.28,-152.69" />
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" android:width="108dp" android:height="108dp" android:viewportWidth="2008.27" android:viewportHeight="2009.62">
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFD22A" android:strokeWidth="217" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M425.1,514.64 H1583.18 A20,20 0,0 1,1603.18 534.64 V1566.53 A20,20 0,0 1,1583.18 1586.53 H425.1 A20,20 0,0 1,405.1 1566.53 V534.64 A20,20 0,0 1,425.1 514.64 Z" />
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFFFFF" android:strokeWidth="150" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M731.72,339.92 H740.93 A27.65,27.65 0,0 1,768.58 367.57 V661.71 A27.65,27.65 0,0 1,740.93 689.36 H731.72 A27.65,27.65 0,0 1,704.07 661.71 V367.57 A27.65,27.65 0,0 1,731.72 339.92 Z M1277.56,339.92 H1286.77 A27.65,27.65 0,0 1,1314.42 367.57 V661.71 A27.65,27.65 0,0 1,1286.77 689.36 H1277.56 A27.65,27.65 0,0 1,1249.91 661.71 V367.57 A27.65,27.65 0,0 1,1277.56 339.92 Z" />
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFFFFF" android:strokeWidth="185" android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M729.01,1004.81 c70.21,112.08 200.6,174.7 327.98,152.69 c122.48,-18.51 206.2,-123.19 222.28,-152.69" />
</vector>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="108dp" android:height="108dp" android:viewportWidth="108" android:viewportHeight="108">
<group android:scaleX="0.035" android:scaleY="0.035" android:translateX="18.855275" android:translateY="18.83165">
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFFFFF" android:strokeWidth="217" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M425.1,514.64 H1583.18 A20,20 0,0 1,1603.18 534.64 V1566.53 A20,20 0,0 1,1583.18 1586.53 H425.1 A20,20 0,0 1,405.1 1566.53 V534.64 A20,20 0,0 1,425.1 514.64 Z" />
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFFFFF" android:strokeWidth="150" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M731.72,339.92 H740.93 A27.65,27.65 0,0 1,768.58 367.57 V661.71 A27.65,27.65 0,0 1,740.93 689.36 H731.72 A27.65,27.65 0,0 1,704.07 661.71 V367.57 A27.65,27.65 0,0 1,731.72 339.92 Z M1277.56,339.92 H1286.77 A27.65,27.65 0,0 1,1314.42 367.57 V661.71 A27.65,27.65 0,0 1,1286.77 689.36 H1277.56 A27.65,27.65 0,0 1,1249.91 661.71 V367.57 A27.65,27.65 0,0 1,1277.56 339.92 Z" />
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFFFFF" android:strokeWidth="185" android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M729.01,1004.81 c70.21,112.08 200.6,174.7 327.98,152.69 c122.48,-18.51 206.2,-123.19 222.28,-152.69" />
</group>
</vector>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="288dp" android:height="288dp" android:viewportWidth="288" android:viewportHeight="288">
<group android:scaleX="0.094" android:scaleY="0.094" android:translateX="49.61131" android:translateY="49.54786">
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFD22A" android:strokeWidth="217" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M425.1,514.64 H1583.18 A20,20 0,0 1,1603.18 534.64 V1566.53 A20,20 0,0 1,1583.18 1586.53 H425.1 A20,20 0,0 1,405.1 1566.53 V534.64 A20,20 0,0 1,425.1 514.64 Z" />
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFFFFF" android:strokeWidth="150" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M731.72,339.92 H740.93 A27.65,27.65 0,0 1,768.58 367.57 V661.71 A27.65,27.65 0,0 1,740.93 689.36 H731.72 A27.65,27.65 0,0 1,704.07 661.71 V367.57 A27.65,27.65 0,0 1,731.72 339.92 Z M1277.56,339.92 H1286.77 A27.65,27.65 0,0 1,1314.42 367.57 V661.71 A27.65,27.65 0,0 1,1286.77 689.36 H1277.56 A27.65,27.65 0,0 1,1249.91 661.71 V367.57 A27.65,27.65 0,0 1,1277.56 339.92 Z" />
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFFFFF" android:strokeWidth="185" android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M729.01,1004.81 c70.21,112.08 200.6,174.7 327.98,152.69 c122.48,-18.51 206.2,-123.19 222.28,-152.69" />
</group>
</vector>
@@ -1,12 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<item android:drawable="@drawable/tpschedule_gradient" />
<item android:width="120dp" android:height="120dp" android:gravity="center" android:drawable="@drawable/ic_tpschedule_logo" />
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" android:width="108dp" android:height="108dp" android:viewportWidth="2008.27" android:viewportHeight="2009.62">
<path android:pathData="M0.5,0.5 H2007.77 V2009.12 H0.5 Z" android:strokeColor="#000000" android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient android:type="linear" android:startX="324.09" android:startY="1572.05" android:endX="1871.99" android:endY="280.93">
<item android:offset="0" android:color="#3FD1FF" />
<item android:offset="0.28" android:color="#38B3FF" />
<item android:offset="1" android:color="#2A6CFF" />
</gradient>
</aapt:attr>
</path>
</vector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/tpschedule_gradient" />
<foreground android:drawable="@drawable/ic_tpschedule_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/tpschedule_gradient" />
<foreground android:drawable="@drawable/ic_tpschedule_foreground" />
<monochrome android:drawable="@drawable/ic_tpschedule_monochrome" />
</adaptive-icon>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" android:width="108dp" android:height="108dp" android:viewportWidth="2008.27" android:viewportHeight="2009.62">
<path android:pathData="M0.5,0.5 H2007.77 V2009.12 H0.5 Z" android:strokeColor="#000000" android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient android:type="linear" android:startX="324.09" android:startY="1572.05" android:endX="1871.99" android:endY="280.93">
<item android:offset="0" android:color="#3FD1FF" />
<item android:offset="0.28" android:color="#38B3FF" />
<item android:offset="1" android:color="#2A6CFF" />
</gradient>
</aapt:attr>
</path>
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFD22A" android:strokeWidth="217" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M425.1,514.64 H1583.18 A20,20 0,0 1,1603.18 534.64 V1566.53 A20,20 0,0 1,1583.18 1586.53 H425.1 A20,20 0,0 1,405.1 1566.53 V534.64 A20,20 0,0 1,425.1 514.64 Z" />
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFFFFF" android:strokeWidth="150" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M731.72,339.92 H740.93 A27.65,27.65 0,0 1,768.58 367.57 V661.71 A27.65,27.65 0,0 1,740.93 689.36 H731.72 A27.65,27.65 0,0 1,704.07 661.71 V367.57 A27.65,27.65 0,0 1,731.72 339.92 Z M1277.56,339.92 H1286.77 A27.65,27.65 0,0 1,1314.42 367.57 V661.71 A27.65,27.65 0,0 1,1286.77 689.36 H1277.56 A27.65,27.65 0,0 1,1249.91 661.71 V367.57 A27.65,27.65 0,0 1,1277.56 339.92 Z" />
<path android:fillColor="@android:color/transparent" android:strokeColor="#FFFFFF" android:strokeWidth="185" android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeMiterLimit="10" android:pathData="M729.01,1004.81 c70.21,112.08 200.6,174.7 327.98,152.69 c122.48,-18.51 206.2,-123.19 222.28,-152.69" />
</vector>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Android 12 requires a solid system splash; Flutter continues the SVG gradient. -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/tpschedule_blue</item>
<item name="android:windowSplashScreenBackground">@color/tpschedule_blue</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_tpschedule_splash</item>
<item name="android:statusBarColor">@color/tpschedule_blue</item>
<item name="android:navigationBarColor">@color/tpschedule_blue</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
</style>
</resources>
@@ -1,18 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:statusBarColor">@color/tpschedule_blue</item>
<item name="android:navigationBarColor">@color/tpschedule_blue</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/tpschedule_blue</item>
<item name="android:statusBarColor">@color/tpschedule_blue</item>
<item name="android:navigationBarColor">@color/tpschedule_blue</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
</style>
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Android 12 requires a solid system splash; Flutter continues the SVG gradient. -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/tpschedule_blue</item>
<item name="android:windowSplashScreenBackground">@color/tpschedule_blue</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_tpschedule_splash</item>
<item name="android:statusBarColor">@color/tpschedule_blue</item>
<item name="android:navigationBarColor">@color/tpschedule_blue</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
</style>
</resources>
@@ -1,18 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:statusBarColor">@color/tpschedule_blue</item>
<item name="android:navigationBarColor">@color/tpschedule_blue</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
<item name="android:windowBackground">@color/tpschedule_blue</item>
<item name="android:statusBarColor">@color/tpschedule_blue</item>
<item name="android:navigationBarColor">@color/tpschedule_blue</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
</style>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="tpschedule_blue">#38B3FF</color>
</resources>
+46
View File
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 2008.27 2009.62">
<defs>
<style>
.cls-1 {
stroke-width: 185px;
}
.cls-1, .cls-2, .cls-3 {
fill: none;
}
.cls-1, .cls-2, .cls-3, .cls-4 {
stroke-miterlimit: 10;
}
.cls-1, .cls-3 {
stroke: #fff;
}
.cls-2 {
stroke: #ffd22a;
stroke-width: 217px;
}
.cls-3 {
stroke-width: 150px;
}
.cls-4 {
fill: url(#_未命名的渐变_7);
stroke: #000;
}
</style>
<linearGradient id="_未命名的渐变_7" data-name="未命名的渐变 7" x1="324.09" y1="1572.05" x2="1871.99" y2="280.93" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#3fd1ff"/>
<stop offset=".28" stop-color="#38b3ff"/>
<stop offset="1" stop-color="#2a6cff"/>
</linearGradient>
</defs>
<rect class="cls-4" x=".5" y=".5" width="2007.27" height="2008.62"/>
<rect class="cls-2" x="405.1" y="514.64" width="1198.08" height="1071.89" rx="20" ry="20"/>
<rect class="cls-3" x="704.07" y="339.92" width="64.51" height="349.44" rx="27.65" ry="27.65"/>
<rect class="cls-3" x="1249.91" y="339.92" width="64.51" height="349.44" rx="27.65" ry="27.65"/>
<path class="cls-1" d="M729.01,1004.81c70.21,112.08,200.6,174.7,327.98,152.69,122.48-18.51,206.2-123.19,222.28-152.69"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+1 -1
View File
@@ -28,7 +28,7 @@ class HepanApp extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp(
debugShowCheckedModeBanner: false,
title: 'RSchedule',
title: 'TPSchedule',
theme: hepanTheme(),
locale: const Locale('zh', 'CN'),
supportedLocales: const [Locale('zh', 'CN')],
+45
View File
@@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../widgets/tp_logo.dart';
/// The first Flutter frame while saved data and reminder services initialize.
class TpLaunchPage extends StatelessWidget {
const TpLaunchPage({super.key});
@override
Widget build(BuildContext context) => AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
statusBarBrightness: Brightness.dark,
systemNavigationBarColor: Color(0xFF38B3FF),
systemNavigationBarIconBrightness: Brightness.light,
),
child: Scaffold(
backgroundColor: const Color(0xFF38B3FF),
body: DecoratedBox(
decoration: const BoxDecoration(gradient: tpBrandGradient),
child: SizedBox.expand(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const TpLogo(size: 120, background: false),
const SizedBox(height: 20),
Text(
'TPSchedule',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
letterSpacing: 0.4,
),
),
],
),
),
),
),
),
);
}
+48
View File
@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'tp_launch_page.dart';
/// Displays the brand during real initialization, without a minimum wait.
class TpStartup extends StatelessWidget {
const TpStartup({super.key, required this.ready, required this.child});
final Future<void> ready;
final Widget child;
@override
Widget build(BuildContext context) => FutureBuilder<void>(
future: ready,
builder: (context, snapshot) {
final complete = snapshot.connectionState == ConnectionState.done;
final reduceMotion =
MediaQuery.maybeOf(context)?.disableAnimations ??
WidgetsBinding
.instance
.platformDispatcher
.accessibilityFeatures
.disableAnimations;
return AnimatedSwitcher(
duration: reduceMotion
? Duration.zero
: const Duration(milliseconds: 220),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
child: complete
? AnnotatedRegion<SystemUiOverlayStyle>(
key: const ValueKey('tp-ready'),
value: SystemUiOverlayStyle.dark.copyWith(
statusBarColor: Colors.transparent,
systemNavigationBarColor: const Color(0xFFF3F7FF),
systemNavigationBarIconBrightness: Brightness.dark,
),
child: child,
)
: const MaterialApp(
key: ValueKey('tp-launch'),
debugShowCheckedModeBanner: false,
home: TpLaunchPage(),
),
);
},
);
}
@@ -70,7 +70,7 @@ class ProfilePage extends StatelessWidget {
),
children: [
const Text(
'RSchedule',
'TPSchedule',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
),
const SizedBox(height: 16),
@@ -220,7 +220,7 @@ class ProfilePage extends StatelessWidget {
child: Column(
children: [
const Text(
'RSchedule',
'TPSchedule',
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
),
const SizedBox(height: 6),
@@ -94,7 +94,7 @@ class _SettingsPageState extends State<SettingsPage> {
context: context,
builder: (context) => AlertDialog(
title: const Text('关闭日历同步'),
content: const Text('是否删除Riverside Schedule 创建的课程日程?其他日程不会受影响。'),
content: const Text('是否删除 TPSchedule 创建的课程日程?其他日程不会受影响。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
+8 -5
View File
@@ -4,6 +4,7 @@ import 'package:timezone/data/latest.dart' as tzdata;
import 'app/app_controller.dart';
import 'app/hepan_app.dart';
import 'app/tp_startup.dart';
import 'data/file_snapshot_store.dart';
import 'services/local_notification_service.dart';
import 'services/android_calendar_service.dart';
@@ -33,12 +34,14 @@ Future<void> main() async {
AndroidCalendarService(MethodCalendarGateway()),
DateTime.now,
);
await controller.initialize();
runApp(
buildApplication(
controller,
notificationSelection: selection,
school: SchoolSession(),
TpStartup(
ready: controller.initialize(),
child: buildApplication(
controller,
notificationSelection: selection,
school: SchoolSession(),
),
),
);
}
@@ -87,7 +87,7 @@ class AndroidCalendarService extends CalendarService {
calendarId: calendarId,
title: occurrence.course.name,
room: occurrence.course.room,
description: '任课老师:${occurrence.course.teacher}\nRiverside Schedule',
description: '任课老师:${occurrence.course.teacher}\nTPSchedule',
startMs: occurrence.start.millisecondsSinceEpoch,
endMs: occurrence.end.millisecondsSinceEpoch,
),
+30 -13
View File
@@ -126,11 +126,11 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
colors: lens
? [
const Color(0xFFD8DBE0)
.withValues(alpha: 0.72 * (1 - pressure * 0.91)),
.withValues(alpha: 0.14 * (1 - pressure * 0.82)),
const Color(0xFFC3C7CE)
.withValues(alpha: 0.60 * (1 - pressure * 0.93)),
.withValues(alpha: 0.08 * (1 - pressure * 0.82)),
const Color(0xFFE5E7EB)
.withValues(alpha: 0.72 * (1 - pressure * 0.90)),
.withValues(alpha: 0.12 * (1 - pressure * 0.82)),
]
: [
Colors.white.withValues(alpha: 0.028),
@@ -145,7 +145,8 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
return ClipRRect(
borderRadius: BorderRadius.circular(40),
child: GlassRefraction(
strength: lens ? 0.25 + pressure * 0.95 : 0.32,
strength: lens ? 0.70 + pressure * 0.70 : 0.55,
blurSigma: lens ? 1.6 - pressure * 0.6 : 2,
child: content,
),
);
@@ -177,8 +178,8 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
borderRadius: BorderRadius.circular(40),
boxShadow: [
BoxShadow(
color: _blue.withValues(alpha: 0.035),
blurRadius: 12,
color: const Color(0xFF4C6078).withValues(alpha: 0.075),
blurRadius: 16,
offset: const Offset(0, 5),
),
],
@@ -288,15 +289,14 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
);
}
/// Thin paired rims and directional glints suggest rounded optical thickness.
/// All strong highlights stay on the edge; the centre remains clear.
/// A diffuse silver bevel and thin directional glints show optical thickness.
/// The painted highlights stay on the edge so background colour passes through.
class _GlassRimPainter extends CustomPainter {
const _GlassRimPainter({required this.lens});
final bool lens;
static const _blue = Color(0xFF007AFF);
@override
void paint(Canvas canvas, Size size) {
final edge = lens ? const Color(0xFF7B838F) : _blue;
final edge = lens ? const Color(0xFF7B838F) : const Color(0xFFAFBECF);
final bounds = Offset.zero & size;
final outer = RRect.fromRectAndRadius(
bounds.deflate(0.7),
@@ -306,6 +306,23 @@ class _GlassRimPainter extends CustomPainter {
bounds.deflate(2.2),
Radius.circular(size.height / 2 - 1.5),
);
canvas.drawRRect(
outer,
Paint()
..style = PaintingStyle.stroke
..strokeWidth = lens ? 1.8 : 2.2
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 0.9)
..shader = ui.Gradient.linear(
bounds.topLeft,
bounds.bottomRight,
[
Colors.white.withValues(alpha: 0.24),
edge.withValues(alpha: 0.04),
Colors.white.withValues(alpha: 0.14),
],
[0.0, 0.55, 1.0],
),
);
canvas.drawRRect(
outer,
Paint()
@@ -336,9 +353,9 @@ class _GlassRimPainter extends CustomPainter {
bounds.topLeft,
bounds.bottomRight,
[
Colors.white.withValues(alpha: 0.30),
Colors.white.withValues(alpha: 0.20),
edge.withValues(alpha: lens ? 0.13 : 0.07),
Colors.white.withValues(alpha: 0.40),
Colors.white.withValues(alpha: 0.28),
],
[0.0, 0.5, 1.0],
),
@@ -362,7 +379,7 @@ class _GlassRimPainter extends CustomPainter {
Offset(size.width, 0),
[
Colors.white.withValues(alpha: 0),
Colors.white.withValues(alpha: 0.65),
Colors.white.withValues(alpha: 0.50),
Colors.white.withValues(alpha: 0),
],
[0.0, 0.5, 1.0],
+43 -11
View File
@@ -8,9 +8,13 @@ class GlassRefraction extends StatefulWidget {
const GlassRefraction({
super.key,
required this.strength,
this.blurSigma = 2,
required this.child,
});
final double strength;
/// Optical softness in logical pixels, independent of display density.
final double blurSigma;
final Widget child;
@override
State<GlassRefraction> createState() => _GlassRefractionState();
@@ -18,6 +22,7 @@ class GlassRefraction extends StatefulWidget {
class _GlassRefractionState extends State<GlassRefraction> {
static Future<ui.FragmentProgram>? _program;
static ui.FragmentProgram? _loadedProgram;
ui.FragmentShader? _shader;
@override
void initState() {
@@ -27,12 +32,12 @@ class _GlassRefractionState extends State<GlassRefraction> {
Future<void> _load() async {
try {
final program = await (_program ??= ui.FragmentProgram.fromAsset(
'shaders/liquid_glass.frag',
));
final program = _loadedProgram ??= await (_program ??=
ui.FragmentProgram.fromAsset('shaders/liquid_glass.frag'));
if (mounted) setState(() => _shader = program.fragmentShader());
} catch (error) {
_program = null;
_loadedProgram = null;
debugPrint('Glass refraction unavailable: $error');
}
}
@@ -46,12 +51,17 @@ class _GlassRefractionState extends State<GlassRefraction> {
@override
Widget build(BuildContext context) => _shader == null
? BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 0.6, sigmaY: 0.6),
filter: ui.ImageFilter.blur(
sigmaX: widget.blurSigma,
sigmaY: widget.blurSigma,
tileMode: TileMode.clamp,
),
child: widget.child,
)
: _LiveGlassFilter(
shader: _shader!,
strength: widget.strength,
blurSigma: widget.blurSigma,
pixelRatio: MediaQuery.devicePixelRatioOf(context),
child: widget.child,
);
@@ -61,29 +71,41 @@ class _LiveGlassFilter extends SingleChildRenderObjectWidget {
const _LiveGlassFilter({
required this.shader,
required this.strength,
required this.blurSigma,
required this.pixelRatio,
required super.child,
});
final ui.FragmentShader shader;
final double strength, pixelRatio;
final double strength, blurSigma, pixelRatio;
@override
_RenderLiveGlass createRenderObject(BuildContext context) =>
_RenderLiveGlass(shader, strength, pixelRatio);
_RenderLiveGlass(shader, strength, blurSigma, pixelRatio);
@override
void updateRenderObject(
BuildContext context,
_RenderLiveGlass renderObject,
) => renderObject.update(shader, strength, pixelRatio);
) => renderObject.update(shader, strength, blurSigma, pixelRatio);
}
class _RenderLiveGlass extends RenderProxyBox {
_RenderLiveGlass(this._shader, this._strength, this._pixelRatio);
_RenderLiveGlass(
this._shader,
this._strength,
this._blurSigma,
this._pixelRatio,
);
ui.FragmentShader _shader;
double _strength, _pixelRatio;
double _strength, _blurSigma, _pixelRatio;
final _layer = LayerHandle<BackdropFilterLayer>();
void update(ui.FragmentShader shader, double strength, double pixelRatio) {
void update(
ui.FragmentShader shader,
double strength,
double blurSigma,
double pixelRatio,
) {
_shader = shader;
_strength = strength;
_blurSigma = blurSigma;
_pixelRatio = pixelRatio;
markNeedsPaint();
}
@@ -103,7 +125,17 @@ class _RenderLiveGlass extends RenderProxyBox {
..setFloat(5, size.height * _pixelRatio)
..setFloat(6, _strength);
_layer.layer ??= BackdropFilterLayer();
_layer.layer!.filter = ui.ImageFilter.shader(_shader);
// Refract the original backdrop first, then diffuse it with the engine's
// separable Gaussian blur. Keeping the shader inside the composition also
// keeps its source texture coordinates independent of blur padding.
_layer.layer!.filter = ui.ImageFilter.compose(
outer: ui.ImageFilter.blur(
sigmaX: _blurSigma,
sigmaY: _blurSigma,
tileMode: TileMode.clamp,
),
inner: ui.ImageFilter.shader(_shader),
);
context.pushLayer(_layer.layer!, super.paint, offset);
}
+93
View File
@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
/// The original APP.svg gradient, expressed in its view-box coordinates.
const tpBrandGradient = LinearGradient(
begin: Alignment(2 * 324.09 / 2008.27 - 1, 2 * 1572.05 / 2009.62 - 1),
end: Alignment(2 * 1871.99 / 2008.27 - 1, 2 * 280.93 / 2009.62 - 1),
colors: [Color(0xFF3FD1FF), Color(0xFF38B3FF), Color(0xFF2A6CFF)],
stops: [0, 0.28, 1],
);
/// Vector rendering of assets/branding/APP.svg; no raster scaling or redesign.
class TpLogo extends StatelessWidget {
const TpLogo({super.key, this.size = 128, this.background = true});
final double size;
final bool background;
@override
Widget build(BuildContext context) => Semantics(
label: 'TPSchedule 标志',
image: true,
child: SizedBox.square(
dimension: size,
child: CustomPaint(painter: _TpLogoPainter(background: background)),
),
);
}
class _TpLogoPainter extends CustomPainter {
const _TpLogoPainter({required this.background});
final bool background;
@override
void paint(Canvas canvas, Size size) {
canvas.save();
canvas.scale(size.width / 2008.27, size.height / 2009.62);
if (background) {
const bounds = Rect.fromLTWH(0.5, 0.5, 2007.27, 2008.62);
canvas.drawRect(
bounds,
Paint()
..shader = tpBrandGradient.createShader(
const Rect.fromLTWH(0, 0, 2008.27, 2009.62),
),
);
canvas.drawRect(
bounds,
Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1
..color = Colors.black,
);
}
final stroke = Paint()
..style = PaintingStyle.stroke
..strokeJoin = StrokeJoin.miter
..strokeMiterLimit = 10
..strokeCap = StrokeCap.butt;
canvas.drawRRect(
RRect.fromRectAndRadius(
const Rect.fromLTWH(405.1, 514.64, 1198.08, 1071.89),
const Radius.circular(20),
),
stroke
..color = const Color(0xFFFFD22A)
..strokeWidth = 217,
);
stroke
..color = Colors.white
..strokeWidth = 150;
for (final x in [704.07, 1249.91]) {
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(x, 339.92, 64.51, 349.44),
const Radius.circular(27.65),
),
stroke,
);
}
canvas.drawPath(
Path()
..moveTo(729.01, 1004.81)
..relativeCubicTo(70.21, 112.08, 200.6, 174.7, 327.98, 152.69)
..relativeCubicTo(122.48, -18.51, 206.2, -123.19, 222.28, -152.69),
stroke..strokeWidth = 185,
);
canvas.restore();
}
@override
bool shouldRepaint(_TpLogoPainter oldDelegate) =>
background != oldDelegate.background;
}
+4 -2
View File
@@ -1,7 +1,7 @@
name: hepan_everyday
description: RSchedule 研究生课表
description: TPSchedule 学生课表
publish_to: none
version: 0.4.0+7
version: 0.5.0+8
environment:
sdk: ^3.13.5
dependencies:
@@ -24,5 +24,7 @@ dev_dependencies:
flutter_lints: ^6.0.0
flutter:
uses-material-design: true
assets:
- assets/branding/
shaders:
- shaders/liquid_glass.frag
+8 -2
View File
@@ -34,8 +34,14 @@ void main() {
float dispersion = u_strength * rim * 0.65;
colour.r = texture(u_background, sampleUV(ray + normal * dispersion)).r;
colour.b = texture(u_background, sampleUV(ray - normal * dispersion)).b;
float reflection = pow(rim, 4.0) * u_strength * 0.10
* max(0.0, dot(normal, normalize(vec2(-0.5, -0.8))));
// The curved rim catches nearby background light as well as transmitting it.
// Reflection follows live pixels instead of being a painted white outline.
float facing = max(0.0, dot(normal, normalize(vec2(-0.5, -0.8))));
float edge_light = pow(rim, 4.0) * u_strength * facing;
vec4 reflected = texture(u_background,
sampleUV(point + normal * radius * 0.40));
colour = mix(colour, reflected, min(edge_light * 0.18, 0.22));
float reflection = edge_light * 0.07;
colour.rgb = min(colour.rgb + reflection * colour.a, vec3(colour.a));
frag_color = colour;
}
+1 -6
View File
@@ -86,12 +86,7 @@ void main() {
),
);
await tester.pumpAndSettle();
final lens = find
.descendant(
of: find.byType(GlassDock),
matching: find.byType(BackdropFilter),
)
.last;
final lens = find.byType(GlassRefraction).last;
final resting = tester.getSize(lens);
final gesture = await tester.startGesture(
tester.getCenter(find.byKey(const Key('dock-cursor'))),
@@ -0,0 +1,270 @@
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hepan_everyday/widgets/glass_dock.dart';
import 'package:hepan_everyday/widgets/glass_refraction.dart';
void main() {
testWidgets(
'glass softens fine background detail without whitening its colour',
(tester) async {
final pixels = await _render(
tester,
const GlassRefraction(strength: 0.32, child: SizedBox.expand()),
stripes: true,
);
final reds = [for (var x = 110; x < 150; x++) _channel(pixels, x, 34, 0)];
final contrast =
reds.reduce((a, b) => a > b ? a : b) -
reds.reduce((a, b) => a < b ? a : b);
expect(contrast, lessThan(25));
expect(_channel(pixels, 132, 34, 0), closeTo(130, 15));
expect(_channel(pixels, 132, 34, 1), closeTo(120, 15));
expect(_channel(pixels, 132, 34, 2), closeTo(140, 15));
expect(_channel(pixels, 132, 34, 3), 255);
},
);
testWidgets(
'Dock lens preserves the backdrop colour through its neutral tint',
(tester) async {
final pixels = await _render(
tester,
GlassDock(selectedIndex: 0, onSelected: (_) {}),
);
// Below the symbol and away from the rim, the selected lens should retain
// most of this saturated cyan backdrop instead of painting a grey pill.
final red = _channel(pixels, 46, 52, 0);
final green = _channel(pixels, 46, 52, 1);
final blue = _channel(pixels, 46, 52, 2);
expect(red, lessThan(50));
expect(green - red, greaterThan(130));
expect(blue - red, greaterThan(165));
},
);
testWidgets('Dock base and selected lens both soften the live backdrop', (
tester,
) async {
final pixels = await _render(
tester,
GlassDock(selectedIndex: 0, onSelected: (_) {}),
stripes: true,
preview: true,
);
for (final start in [32, 160]) {
final reds = [
for (var x = start; x < start + 20; x++) _channel(pixels, x, 52, 0),
];
final contrast =
reds.reduce((a, b) => a > b ? a : b) -
reds.reduce((a, b) => a < b ? a : b);
expect(contrast, lessThan(30), reason: 'background at x=$start');
}
});
testWidgets(
'Dock keeps larger backdrop details recognizable through its glass',
(tester) async {
final pixels = await _render(
tester,
GlassDock(selectedIndex: 0, onSelected: (_) {}),
stripes: true,
stripeWidth: 12,
preview: true,
);
for (final start in [24, 160]) {
final reds = [
for (var x = start; x < start + 40; x++) _channel(pixels, x, 52, 0),
];
final contrast =
reds.reduce((a, b) => a > b ? a : b) -
reds.reduce((a, b) => a < b ? a : b);
// The original stripes have 180 levels of contrast. Both the base and
// selected lens retain over half, keeping these larger shapes readable.
expect(contrast, greaterThan(90), reason: 'background at x=$start');
}
},
);
testWidgets(
'composed diffusion keeps edge refraction aligned when glass moves',
(tester) async {
final plain = await _render(
tester,
const GlassRefraction(strength: 0, child: SizedBox.expand()),
gradient: true,
);
final refracted = await _render(
tester,
const GlassRefraction(strength: 1, child: SizedBox.expand()),
gradient: true,
);
final translated = await _render(
tester,
const GlassRefraction(strength: 1, child: SizedBox.expand()),
gradient: true,
offset: const Offset(23, -37),
);
expect(
(_channel(refracted, 15, 34, 0) - _channel(plain, 15, 34, 0)).abs(),
greaterThan(3),
);
expect(
_channel(refracted, 132, 34, 0),
closeTo(_channel(plain, 132, 34, 0), 2),
);
for (final x in [15, 132, 248]) {
expect(
_channel(translated, x, 34, 0),
closeTo(_channel(refracted, x, 34, 0), 2),
);
}
},
skip: !ui.ImageFilter.isShaderFilterSupported,
);
}
int _channel(ByteData pixels, int x, int y, int channel) =>
pixels.getUint8((y * 264 + x) * 4 + channel);
Future<ByteData> _render(
WidgetTester tester,
Widget glass, {
bool stripes = false,
double stripeWidth = 2,
bool gradient = false,
bool preview = false,
Offset offset = Offset.zero,
}) async {
tester.view.physicalSize = const Size(800, 600);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final key = GlobalKey(), glassKey = GlobalKey();
await tester.pumpWidget(
RepaintBoundary(
key: key,
child: MaterialApp(
home: Center(
child: Transform.translate(
offset: offset,
child: SizedBox(
key: glassKey,
width: 264,
height: 68,
child: Stack(
children: [
Positioned.fill(
child: CustomPaint(
painter: _Backdrop(stripes, gradient, stripeWidth),
),
),
Positioned.fill(child: glass),
],
),
),
),
),
),
),
);
if (ui.ImageFilter.isShaderFilterSupported) {
// Native shader loading completes outside the widget test's fake async zone.
await tester.runAsync(
() => ui.FragmentProgram.fromAsset('shaders/liquid_glass.frag'),
);
}
await tester.pumpAndSettle();
if (ui.ImageFilter.isShaderFilterSupported) {
expect(find.byType(BackdropFilter), findsNothing);
}
return (await tester.runAsync(() async {
final boundary =
key.currentContext!.findRenderObject()! as RenderRepaintBoundary;
final image = await boundary.toImage(pixelRatio: 1);
final data = (await image.toByteData(format: ui.ImageByteFormat.rawRgba))!;
final region = tester.getRect(find.byKey(glassKey));
if (preview && const bool.fromEnvironment('TP_BRAND_PREVIEW')) {
final recorder = ui.PictureRecorder();
ui.Canvas(recorder).drawImageRect(
image,
region,
const Rect.fromLTWH(0, 0, 792, 204),
Paint()..filterQuality = FilterQuality.high,
);
final picture = recorder.endRecording();
final previewImage = await picture.toImage(792, 204);
final png = (await previewImage.toByteData(
format: ui.ImageByteFormat.png,
))!;
final output = File('../dist/screenshots/glass-frosted-dock.png');
await output.parent.create(recursive: true);
await output.writeAsBytes(png.buffer.asUint8List());
previewImage.dispose();
picture.dispose();
}
final cropped = ByteData(264 * 68 * 4);
for (var y = 0; y < 68; y++) {
for (var x = 0; x < 264; x++) {
final source =
((region.top.round() + y) * image.width + region.left.round() + x) *
4;
for (var channel = 0; channel < 4; channel++) {
cropped.setUint8(
(y * 264 + x) * 4 + channel,
data.getUint8(source + channel),
);
}
}
}
image.dispose();
return cropped;
}))!;
}
class _Backdrop extends CustomPainter {
const _Backdrop(this.stripes, this.gradient, this.stripeWidth);
final bool stripes;
final bool gradient;
final double stripeWidth;
@override
void paint(Canvas canvas, Size size) {
if (gradient) {
for (var x = 0; x < size.width; x++) {
canvas.drawRect(
Rect.fromLTWH(x.toDouble(), 0, 1, size.height),
Paint()..color = Color.fromARGB(255, 40 + (x * 0.7).round(), 70, 90),
);
}
return;
}
if (!stripes) {
canvas.drawRect(
Offset.zero & size,
Paint()..color = const Color(0xFF14B4E6),
);
return;
}
for (var x = 0.0; x < size.width; x += stripeWidth) {
canvas.drawRect(
Rect.fromLTWH(x, 0, stripeWidth, size.height),
Paint()
..color = (x / stripeWidth).round().isEven
? const Color(0xFFDC3C3C)
: const Color(0xFF28B4DC),
);
}
}
@override
bool shouldRepaint(_Backdrop oldDelegate) =>
oldDelegate.stripes != stripes ||
oldDelegate.gradient != gradient ||
oldDelegate.stripeWidth != stripeWidth;
}
@@ -58,4 +58,63 @@ void main() {
sourcePicture.dispose();
},
);
test(
'glass rim reflects nearby backdrop light outside its transmitted ray',
() async {
const width = 160, height = 100;
final program = await ui.FragmentProgram.fromAsset(
'shaders/liquid_glass.frag',
);
Future<ByteData> draw(bool brightEdge) async {
final sourceRecorder = ui.PictureRecorder();
final sourceCanvas = ui.Canvas(sourceRecorder);
sourceCanvas.drawRect(
const ui.Rect.fromLTWH(0, 0, 160, 100),
ui.Paint()..color = const ui.Color(0xFF28465A),
);
if (brightEdge) {
sourceCanvas.drawRect(
const ui.Rect.fromLTWH(0, 0, 5, 100),
ui.Paint()..color = const ui.Color(0xFF28E65A),
);
}
final sourcePicture = sourceRecorder.endRecording();
final source = await sourcePicture.toImage(width, height);
final shader = program.fragmentShader()
..setFloat(0, width.toDouble())
..setFloat(1, height.toDouble())
..setFloat(2, 0)
..setFloat(3, 0)
..setFloat(4, width.toDouble())
..setFloat(5, height.toDouble())
..setFloat(6, 1)
..setImageSampler(0, source);
final recorder = ui.PictureRecorder();
ui.Canvas(recorder).drawRect(
const ui.Rect.fromLTWH(0, 0, 160, 100),
ui.Paint()..shader = shader,
);
final picture = recorder.endRecording();
final image = await picture.toImage(width, height);
final data = (await image.toByteData())!;
image.dispose();
picture.dispose();
shader.dispose();
source.dispose();
sourcePicture.dispose();
return data;
}
final plain = await draw(false), reflected = await draw(true);
final rimGreen = (50 * width + 8) * 4 + 1;
final centreGreen = (50 * width + 80) * 4 + 1;
expect(
reflected.getUint8(rimGreen) - plain.getUint8(rimGreen),
greaterThan(2),
);
expect(reflected.getUint8(centreGreen), plain.getUint8(centreGreen));
expect(reflected.getUint8(rimGreen + 2), 255);
},
);
}
+1 -1
View File
@@ -72,7 +72,7 @@ void main() {
await tester.tap(find.bySemanticsLabel('个人设置'));
await tester.pumpAndSettle();
expect(find.text('更新学期'), findsOneWidget);
expect(find.text('RSchedule'), findsWidgets);
expect(find.text('TPSchedule'), findsWidgets);
c.dispose();
},
);
+102
View File
@@ -0,0 +1,102 @@
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hepan_everyday/app/tp_launch_page.dart';
import 'package:hepan_everyday/widgets/tp_logo.dart';
void main() {
testWidgets('logo preserves calendar, bindings and smile from supplied art', (
tester,
) async {
final boundaryKey = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Center(
child: RepaintBoundary(
key: boundaryKey,
child: const TpLogo(size: 200),
),
),
),
);
final pixels = await _capture(tester, boundaryKey, 'tpschedule-logo');
expect(_pixel(pixels, 200, 40, 100), [255, 210, 42, 255]);
expect(_pixel(pixels, 200, 73, 50), [255, 255, 255, 255]);
expect(_pixel(pixels, 200, 100, 115), [255, 255, 255, 255]);
final background = _pixel(pixels, 200, 10, 100);
expect(background[2], closeTo(255, 3));
expect(background[0], inInclusiveRange(42, 63));
expect(background[1], inInclusiveRange(108, 209));
});
testWidgets('foreground logo leaves its surround transparent', (
tester,
) async {
final boundaryKey = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Center(
child: RepaintBoundary(
key: boundaryKey,
child: const TpLogo(size: 200, background: false),
),
),
),
);
final pixels = await _capture(tester, boundaryKey);
expect(_pixel(pixels, 200, 10, 10)[3], 0);
expect(_pixel(pixels, 200, 40, 100), [255, 210, 42, 255]);
});
testWidgets('launch fills the screen with brand blue in dark mode', (
tester,
) async {
final boundaryKey = GlobalKey();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.dark(),
home: RepaintBoundary(key: boundaryKey, child: const TpLaunchPage()),
),
);
expect(find.text('TPSchedule'), findsOneWidget);
final logo = tester.widget<TpLogo>(find.byType(TpLogo));
expect(logo.background, isFalse);
final pixels = await _capture(tester, boundaryKey, 'tpschedule-launch');
for (final point in [const Offset(10, 10), const Offset(790, 590)]) {
final pixel = _pixel(pixels, 800, point.dx.toInt(), point.dy.toInt());
expect(pixel[2], closeTo(255, 3));
expect(pixel[0], inInclusiveRange(42, 63));
expect(pixel[1], inInclusiveRange(108, 209));
expect(pixel[3], 255);
}
expect(tester.takeException(), isNull);
});
}
Future<Uint8List> _capture(
WidgetTester tester,
GlobalKey key, [
String? previewName,
]) async {
final boundary =
key.currentContext!.findRenderObject()! as RenderRepaintBoundary;
return (await tester.runAsync(() async {
final image = await boundary.toImage(pixelRatio: 1);
final bytes = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
if (const bool.fromEnvironment('TP_BRAND_PREVIEW') && previewName != null) {
final png = await image.toByteData(format: ui.ImageByteFormat.png);
final output = File('../dist/screenshots/$previewName.png');
await output.parent.create(recursive: true);
await output.writeAsBytes(png!.buffer.asUint8List());
}
image.dispose();
return bytes!.buffer.asUint8List();
}))!;
}
List<int> _pixel(Uint8List pixels, int width, int x, int y) =>
pixels.sublist((y * width + x) * 4, (y * width + x) * 4 + 4);
+29
View File
@@ -0,0 +1,29 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hepan_everyday/app/tp_startup.dart';
void main() {
testWidgets(
'startup shows brand while initializing and immediately proceeds when ready',
(tester) async {
final ready = Completer<void>();
await tester.pumpWidget(
TpStartup(
ready: ready.future,
child: const MaterialApp(home: Scaffold(body: Text('已准备好课表'))),
),
);
await tester.pump();
expect(find.text('TPSchedule'), findsOneWidget);
expect(find.text('已准备好课表'), findsNothing);
ready.complete();
await tester.pump();
await tester.pump();
expect(find.text('已准备好课表'), findsOneWidget);
await tester.pumpAndSettle();
expect(find.text('TPSchedule'), findsNothing);
},
);
}