Rebrand as TPSchedule and refine clear liquid glass dock
This commit is contained in:
@@ -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')],
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user