Initial Commit
This commit is contained in:
115
lib/widgets/faded_scroll_view.dart
Normal file
115
lib/widgets/faded_scroll_view.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FadedScrollView extends StatefulWidget {
|
||||
final Widget child;
|
||||
final double fadeHeight;
|
||||
final double easingDistance;
|
||||
final double maxBlur;
|
||||
final ScrollController? controller;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
const FadedScrollView({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.fadeHeight = 20.0,
|
||||
this.easingDistance = 10.0,
|
||||
this.maxBlur = 3.0,
|
||||
this.controller,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FadedScrollView> createState() => _FadedScrollViewState();
|
||||
}
|
||||
|
||||
class _FadedScrollViewState extends State<FadedScrollView> {
|
||||
late ScrollController _scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController = widget.controller ?? ScrollController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (widget.controller == null) {
|
||||
_scrollController.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _scrollController,
|
||||
builder: (context, child) {
|
||||
final double offset = _scrollController.hasClients ? _scrollController.offset : 0.0;
|
||||
final double fadeProgress = (offset / widget.easingDistance).clamp(0.0, 1.0);
|
||||
final double currentBlur = widget.maxBlur * fadeProgress;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
// Main content
|
||||
ShaderMask(
|
||||
shaderCallback: (Rect bounds) {
|
||||
final double fadeStop = widget.fadeHeight / bounds.height;
|
||||
final Color transparentColor = Colors.white.withOpacity(1.0 - fadeProgress);
|
||||
|
||||
return LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
transparentColor,
|
||||
Colors.white,
|
||||
Colors.white,
|
||||
],
|
||||
stops: [0.0, fadeStop, 1.0],
|
||||
).createShader(bounds);
|
||||
},
|
||||
blendMode: BlendMode.dstIn,
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
padding: widget.padding,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
|
||||
// Single blur with extended fade area for smoother transition
|
||||
if (currentBlur > 0)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: widget.fadeHeight * 1.5, // Extend beyond fade area
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: currentBlur * 0.7, // Reduce intensity slightly
|
||||
sigmaY: currentBlur * 0.7,
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.transparent,
|
||||
Colors.transparent,
|
||||
Colors.black.withOpacity(0.0),
|
||||
],
|
||||
stops: [0.0, 0.6, 0.9, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
219
lib/widgets/service_card.dart
Normal file
219
lib/widgets/service_card.dart
Normal file
@@ -0,0 +1,219 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart' hide Colors, Divider, VerticalDivider;
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:proto_portal/scraper.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
class ServiceCard extends StatelessWidget {
|
||||
final String type;
|
||||
final String time;
|
||||
final String diagram;
|
||||
final String customer;
|
||||
final String destination;
|
||||
final String operator;
|
||||
final String vrm;
|
||||
final GtiService? service;
|
||||
final GtiStation? station;
|
||||
final bool showActions;
|
||||
|
||||
// Constructor for individual fields
|
||||
ServiceCard({
|
||||
required this.type,
|
||||
required this.time,
|
||||
required this.diagram,
|
||||
required this.customer,
|
||||
required this.destination,
|
||||
required this.operator,
|
||||
required this.vrm,
|
||||
this.service,
|
||||
this.station,
|
||||
this.showActions = false,
|
||||
});
|
||||
|
||||
// Constructor that takes a GtiService
|
||||
ServiceCard.fromService({
|
||||
required GtiService service,
|
||||
GtiStation? station,
|
||||
bool showActions = false,
|
||||
}) : this.service = service,
|
||||
this.station = station,
|
||||
this.showActions = showActions,
|
||||
type = service.type,
|
||||
time = service.time,
|
||||
diagram = service.coach,
|
||||
customer = service.customer,
|
||||
destination = service.destination,
|
||||
operator = service.operator,
|
||||
vrm = service.vrm;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Convert type code to full word
|
||||
String typeLabel = type;
|
||||
if (type == 'D') {
|
||||
typeLabel = 'Departure';
|
||||
} else if (type == 'A') {
|
||||
typeLabel = 'Arrival';
|
||||
}
|
||||
|
||||
return OutlinedContainer(
|
||||
width: double.infinity,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 2)
|
||||
],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
IntrinsicHeight(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(typeLabel,
|
||||
style: TextStyle(fontSize: 22))
|
||||
.textLarge,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
VerticalDivider(),
|
||||
SizedBox(
|
||||
width: 94,
|
||||
child: _keyvalueRow(
|
||||
key: "Time",
|
||||
value: time,
|
||||
),
|
||||
),
|
||||
VerticalDivider(),
|
||||
SizedBox(
|
||||
width: 130,
|
||||
child: _keyvalueRow(
|
||||
key: "Diagram",
|
||||
value: diagram,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: _keyvalueRow(
|
||||
key: "Customer",
|
||||
value: customer,
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: _keyvalueRow(
|
||||
key: "Destination",
|
||||
value: destination,
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _keyvalueRow(
|
||||
key: "Operator",
|
||||
value: operator,
|
||||
),
|
||||
),
|
||||
VerticalDivider(),
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: _keyvalueRow(
|
||||
key: "VRM",
|
||||
value: vrm.isNotEmpty ? vrm : "N/A",
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (showActions) ...[
|
||||
Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ButtonGroup(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlineButton(
|
||||
onPressed: service != null && station != null
|
||||
? () {
|
||||
// Encode service as base64
|
||||
final serviceJson = jsonEncode(service!.toJson());
|
||||
final serviceBase64 = base64Encode(utf8.encode(serviceJson));
|
||||
|
||||
final stationJson = jsonEncode({
|
||||
'label': station!.label,
|
||||
'crs': station!.crs,
|
||||
});
|
||||
final stationBase64 = base64Encode(utf8.encode(stationJson));
|
||||
|
||||
GoRouter.of(context).push(
|
||||
Uri(
|
||||
path: '/audit',
|
||||
queryParameters: {
|
||||
'service': serviceBase64,
|
||||
'station': stationBase64,
|
||||
},
|
||||
).toString(),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: Text("Audit"),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
onPressed: () {},
|
||||
child: Text("Depart"),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _keyvalueRow({
|
||||
required String key,
|
||||
required String value,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(key).h4,
|
||||
|
||||
Text(value).small
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user