generated from josiadmin/starter-for-flutter
Initial commit
This commit is contained in:
92
lib/ui/components/checkered_background.dart
Normal file
92
lib/ui/components/checkered_background.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:appwrite_flutter_starter_kit/utils/extensions/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Background color for gradients, blur, etc.
|
||||
const Color checkeredBackgroundColor = Color(0xFFFAFAFB);
|
||||
|
||||
/// Max height factor for background.
|
||||
const double heightConstraintFactor = 0.5;
|
||||
|
||||
/// A custom widget that applies a checkered background pattern with gradient effects.
|
||||
class CheckeredBackground extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const CheckeredBackground({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
painter: CheckeredBackgroundPainter(),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom painter that draws a checkered background pattern with vertical and horizontal grid lines,
|
||||
/// along with linear and radial gradient overlays for visual enhancement.
|
||||
class CheckeredBackgroundPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final double gridSize = min(size.width * 0.1, 64);
|
||||
final double lineThickness = 0.75;
|
||||
final double heightConstraint = size.height * heightConstraintFactor;
|
||||
final Paint linePaint = Paint()
|
||||
..color = Colors.grey.applyOpacity(0.3)
|
||||
..strokeWidth = lineThickness;
|
||||
|
||||
// Draw vertical lines
|
||||
for (double x = 0; x <= size.width; x += gridSize) {
|
||||
canvas.drawLine(Offset(x, 0), Offset(x, heightConstraint), linePaint);
|
||||
}
|
||||
|
||||
// Draw horizontal lines
|
||||
for (double y = 0; y <= heightConstraint; y += gridSize) {
|
||||
canvas.drawLine(Offset(0, y), Offset(size.width, y), linePaint);
|
||||
}
|
||||
|
||||
// Apply gradient overlays
|
||||
_drawRadialGradientOverlay(canvas, size);
|
||||
_drawLinearGradientOverlay(canvas, size, heightConstraint);
|
||||
}
|
||||
|
||||
/// Draws a radial gradient overlay over the canvas to create a smooth blend effect.
|
||||
void _drawRadialGradientOverlay(Canvas canvas, Size size) {
|
||||
final Paint paint = Paint()
|
||||
..shader = RadialGradient(
|
||||
colors: [
|
||||
checkeredBackgroundColor.applyOpacity(0.0),
|
||||
checkeredBackgroundColor.applyOpacity(0.4),
|
||||
checkeredBackgroundColor.applyOpacity(0.2),
|
||||
Colors.transparent,
|
||||
],
|
||||
center: Alignment.center,
|
||||
radius: 2.0,
|
||||
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
|
||||
|
||||
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
|
||||
}
|
||||
|
||||
/// Draws a vertical gradient overlay over the canvas to enhance the checkered background's appearance.
|
||||
void _drawLinearGradientOverlay(
|
||||
Canvas canvas, Size size, double heightConstraint) {
|
||||
final Paint paint = Paint()
|
||||
..shader = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
checkeredBackgroundColor,
|
||||
checkeredBackgroundColor.applyOpacity(0.3),
|
||||
checkeredBackgroundColor.applyOpacity(0.5),
|
||||
checkeredBackgroundColor.applyOpacity(0.7),
|
||||
checkeredBackgroundColor,
|
||||
],
|
||||
).createShader(Rect.fromLTWH(0, 0, size.width, heightConstraint));
|
||||
|
||||
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, heightConstraint), paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
508
lib/ui/components/collapsible_bottomsheet.dart
Normal file
508
lib/ui/components/collapsible_bottomsheet.dart
Normal file
@@ -0,0 +1,508 @@
|
||||
import 'package:appwrite_flutter_starter_kit/data/models/log.dart';
|
||||
import 'package:appwrite_flutter_starter_kit/data/models/project_info.dart';
|
||||
import 'package:appwrite_flutter_starter_kit/ui/components/responsive_layout.dart';
|
||||
import 'package:appwrite_flutter_starter_kit/ui/components/single_wrap.dart';
|
||||
import 'package:appwrite_flutter_starter_kit/utils/extensions/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CollapsibleBottomSheet extends StatefulWidget {
|
||||
final String title;
|
||||
final List<Log> logs;
|
||||
final ProjectInfo projectInfo;
|
||||
|
||||
const CollapsibleBottomSheet({
|
||||
super.key,
|
||||
this.title = "Logs",
|
||||
required this.logs,
|
||||
required this.projectInfo,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CollapsibleBottomSheet> createState() => _CollapsibleBottomSheetState();
|
||||
}
|
||||
|
||||
class _CollapsibleBottomSheetState extends State<CollapsibleBottomSheet> {
|
||||
bool isExpanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(
|
||||
top: BorderSide(color: Color(0xFFEDEDF0), width: 1),
|
||||
),
|
||||
),
|
||||
child: SingleWrap(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => isExpanded = !isExpanded),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
// Captures taps on empty spaces too
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Title & Logs Count
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF56565C),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.applyOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
widget.logs.length.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF56565C),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Expand/Collapse Icon
|
||||
AnimatedRotation(
|
||||
turns: isExpanded ? 0.5 : 0.0,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: const Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: Color(0xFF97979B),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
height: isExpanded
|
||||
? MediaQuery.of(context).size.height * 0.575
|
||||
: 0, // Expandable height
|
||||
child: isExpanded
|
||||
? SingleChildScrollView(
|
||||
child: LogsBottomSheet(
|
||||
logs: widget.logs,
|
||||
projectInfo: widget.projectInfo,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LogsBottomSheet extends StatelessWidget {
|
||||
final List<Log> logs;
|
||||
final ProjectInfo projectInfo;
|
||||
|
||||
const LogsBottomSheet({
|
||||
super.key,
|
||||
required this.logs,
|
||||
required this.projectInfo,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveLayout(
|
||||
smallDeviceLayout: Column(
|
||||
children: [
|
||||
// Project Info Section
|
||||
ProjectSection(projectInfo: projectInfo),
|
||||
|
||||
// Logs Table
|
||||
logs.isEmpty
|
||||
? const EmptyLogsSection()
|
||||
: Column(
|
||||
children: [
|
||||
LogsTableHeader(),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: logs.length,
|
||||
itemBuilder: (context, index) {
|
||||
return LogsTableRow(
|
||||
log: logs[index],
|
||||
needsScroll: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
largeDeviceLayout: SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: ProjectSection(projectInfo: projectInfo),
|
||||
),
|
||||
VerticalDivider(
|
||||
width: 1,
|
||||
thickness: 1,
|
||||
color: Color(0xFFEDEDF0),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: logs.isEmpty
|
||||
? EmptyLogsSection()
|
||||
: Column(
|
||||
children: [
|
||||
LogsTableHeader(),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: logs.length,
|
||||
itemBuilder: (context, index) => LogsTableRow(
|
||||
log: logs[index],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class ProjectSection extends StatelessWidget {
|
||||
final ProjectInfo projectInfo;
|
||||
|
||||
const ProjectSection({
|
||||
super.key,
|
||||
required this.projectInfo,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header with Background & Dividers
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFFAFAFB),
|
||||
border: Border.symmetric(
|
||||
horizontal: BorderSide(color: Color(0xFFEDEDF0), width: 1),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Project",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF97979B),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Grid Layout for Project Info
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ProjectRow(title: "Endpoint", value: projectInfo.endpoint),
|
||||
ProjectRow(title: "Project ID", value: projectInfo.projectId),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// **Reusable Row for Project Details**
|
||||
class ProjectRow extends StatelessWidget {
|
||||
final String title;
|
||||
final String value;
|
||||
|
||||
const ProjectRow({super.key, required this.title, required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF97979B),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF56565C),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyLogsSection extends StatelessWidget {
|
||||
const EmptyLogsSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header with Background & Dividers
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFFAFAFB),
|
||||
border: Border.symmetric(
|
||||
horizontal: BorderSide(color: Color(0xFFEDEDF0), width: 1),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Logs",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF97979B),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Empty State Message
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Text(
|
||||
"There are no logs to show",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF56565C),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LogsTableHeader extends StatelessWidget {
|
||||
const LogsTableHeader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFFAFAFB),
|
||||
border: Border.symmetric(
|
||||
horizontal: BorderSide(color: Color(0xFFEDEDF0), width: 1),
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: _columns.map((column) {
|
||||
return SizedBox(
|
||||
width: column.$2, // Fixed width per column
|
||||
child: Text(
|
||||
column.$1,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF97979B),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Column definitions (Equivalent to the `columns` list in Compose)
|
||||
const List<(String, double)> _columns = [
|
||||
("Date", 150),
|
||||
("Status", 80),
|
||||
("Method", 100),
|
||||
("Path", 125),
|
||||
("Response", 300),
|
||||
];
|
||||
|
||||
class LogsTableRow extends StatelessWidget {
|
||||
final Log log;
|
||||
final bool needsScroll;
|
||||
|
||||
const LogsTableRow({
|
||||
super.key,
|
||||
required this.log,
|
||||
this.needsScroll = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 16, left: 16, bottom: 8),
|
||||
child: needsScroll
|
||||
? SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
_buildTextCell(log.date, _columns[0].$2, monospace: true),
|
||||
_buildStatusCell(log.status.toString(), _columns[1].$2),
|
||||
_buildTextCell(log.method, _columns[2].$2,
|
||||
monospace: true),
|
||||
_buildTextCell(log.path, _columns[3].$2, monospace: true),
|
||||
_buildResponseCell(log.response, _columns[4].$2),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
_buildTextCell(log.date, _columns[0].$2, monospace: true),
|
||||
_buildStatusCell(log.status.toString(), _columns[1].$2),
|
||||
_buildTextCell(log.method, _columns[2].$2, monospace: true),
|
||||
_buildTextCell(log.path, _columns[3].$2, monospace: true),
|
||||
Expanded(
|
||||
child:
|
||||
_buildResponseCell(log.response, _columns[4].$2)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(color: Color(0xFFEDEDF0), thickness: 1),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a standard text cell.
|
||||
Widget _buildTextCell(String text, double width, {bool monospace = false}) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: Text(
|
||||
text,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontFamily: monospace ? 'monospace' : null,
|
||||
color: const Color(0xFF56565C),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds the status cell with dynamic colors.
|
||||
Widget _buildStatusCell(String status, double width) {
|
||||
final isSuccess = int.tryParse(status) != null &&
|
||||
(200 <= int.parse(status) && int.parse(status) <= 399);
|
||||
|
||||
final color = isSuccess ? const Color(0xFF0A714F) : const Color(0xFFB31212);
|
||||
|
||||
final bgColor =
|
||||
isSuccess ? const Color(0x4010B981) : const Color(0x40FF453A);
|
||||
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: SingleWrap(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds the response cell with a gray background.
|
||||
Widget _buildResponseCell(String response, double width) {
|
||||
return SingleWrap(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.applyOpacity(0.25),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Tooltip(
|
||||
message: response.replaceAll(". ", ".\n"),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Text(
|
||||
response.length >= 50 ? response.substring(0, 50) : response,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF56565C),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
109
lib/ui/components/connection_line.dart
Normal file
109
lib/ui/components/connection_line.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'package:appwrite_flutter_starter_kit/utils/extensions/build_context.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A widget that animates a connection line with a checkmark in the middle.
|
||||
/// The left and right lines expand and contract based on the `show` state, with a tick appearing after a delay.
|
||||
///
|
||||
/// [show] - Controls whether the connection line animation and the tick are visible.
|
||||
class ConnectionLine extends StatelessWidget {
|
||||
final bool show;
|
||||
|
||||
const ConnectionLine({super.key, required this.show});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: context.widthFactor(
|
||||
mobileFactor: 0.25,
|
||||
largeScreenFactor: 0.1,
|
||||
),
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
children: [
|
||||
// Left sideline with smooth expansion
|
||||
AnimatedSideline(left: true, show: show),
|
||||
|
||||
// Animated checkmark
|
||||
AnimatedCheckmark(show: show),
|
||||
|
||||
// Right sideline with smooth expansion
|
||||
AnimatedSideline(left: false, show: show),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that animates horizontal sidelines with a gradient effect.
|
||||
/// Handles both visibility and width expansion smoothly.
|
||||
///
|
||||
/// [left] - Indicates whether the sideline is on the left side (true) or right side (false).
|
||||
/// [show] - Controls the visibility and animation of the sideline.
|
||||
class AnimatedSideline extends StatelessWidget {
|
||||
final bool left;
|
||||
final bool show;
|
||||
|
||||
const AnimatedSideline({super.key, required this.left, required this.show});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
return Align(
|
||||
alignment: !left ? Alignment.centerLeft : Alignment.centerRight,
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 750),
|
||||
width: show ? constraints.maxWidth : 0,
|
||||
child: SizedBox(
|
||||
height: 1.5,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: left
|
||||
? [
|
||||
const Color(0x26FE9567),
|
||||
const Color(0xFFF02E65),
|
||||
]
|
||||
: [
|
||||
const Color(0xFFF02E65),
|
||||
const Color(0x26FE9567),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that animates the checkmark icon's appearance with a fade-in effect.
|
||||
///
|
||||
/// [show] - Determines when the checkmark appears.
|
||||
class AnimatedCheckmark extends StatelessWidget {
|
||||
final bool show;
|
||||
|
||||
const AnimatedCheckmark({super.key, required this.show});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedOpacity(
|
||||
opacity: show ? 1.0 : 0.0,
|
||||
duration: Duration(milliseconds: 500),
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: const Color(0x14F02E65),
|
||||
border: Border.all(color: const Color(0x80F02E65), width: 1.8),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(Icons.check, size: 15, color: Color(0xFFFD366E)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
175
lib/ui/components/connection_status_view.dart
Normal file
175
lib/ui/components/connection_status_view.dart
Normal file
@@ -0,0 +1,175 @@
|
||||
import 'package:appwrite_flutter_starter_kit/data/models/status.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// A widget that displays the current connection status and allows the user to send a ping.
|
||||
class ConnectionStatusView extends StatelessWidget {
|
||||
final Status status;
|
||||
final VoidCallback onButtonClick;
|
||||
|
||||
const ConnectionStatusView({
|
||||
super.key,
|
||||
required this.status,
|
||||
required this.onButtonClick,
|
||||
});
|
||||
|
||||
/// Simulates sending a ping with a loading state.
|
||||
Future<void> _sendPing() async {
|
||||
HapticFeedback.mediumImpact();
|
||||
onButtonClick();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 80,
|
||||
child: AnimatedSwitcher(
|
||||
duration: kThemeAnimationDuration,
|
||||
child: _buildStatusContent(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
opacity: status == Status.loading ? 0.0 : 1.0,
|
||||
child: ElevatedButton(
|
||||
onPressed: status == Status.loading ? null : _sendPing,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFD366E),
|
||||
disabledBackgroundColor: const Color(0xFFEDEDF0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
minimumSize: const Size(0, 32),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 6,
|
||||
),
|
||||
child: Text(
|
||||
"Send a ping",
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds the status message UI dynamically.
|
||||
Widget _buildStatusContent() {
|
||||
return Align(
|
||||
key: ValueKey(status),
|
||||
alignment: Alignment.center,
|
||||
child: switch (status) {
|
||||
Status.loading => _buildLoadingIndicator(),
|
||||
Status.success => _buildSuccessMessage(),
|
||||
Status.error => _buildIdleMessage(),
|
||||
Status.idle || Status.error => _buildIdleMessage(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Loading State
|
||||
Widget _buildLoadingIndicator() {
|
||||
return Row(
|
||||
key: const ValueKey(Status.loading),
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Text(
|
||||
"Waiting for connection...",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
height: 19.6 / 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF2D2D31),
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Success State
|
||||
Widget _buildSuccessMessage() {
|
||||
return Column(
|
||||
key: const ValueKey(Status.success),
|
||||
children: const [
|
||||
Text(
|
||||
"Congratulations!",
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF2D2D31),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
"You connected your app successfully.",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
height: 19.6 / 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF56565C),
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Idle State
|
||||
Widget _buildIdleMessage() {
|
||||
return Column(
|
||||
key: const ValueKey(Status.idle),
|
||||
children: const [
|
||||
Text(
|
||||
"Check connection",
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
height: 28.8 / 24,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF2D2D31),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
"Send a ping to verify the connection.",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
height: 19.6 / 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF56565C),
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
205
lib/ui/components/getting_started_cards.dart
Normal file
205
lib/ui/components/getting_started_cards.dart
Normal file
@@ -0,0 +1,205 @@
|
||||
import 'package:appwrite_flutter_starter_kit/ui/components/responsive_layout.dart';
|
||||
import 'package:appwrite_flutter_starter_kit/utils/extensions/build_context.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
/// A widget that contains a list of informational cards displayed vertically.
|
||||
class GettingStartedCards extends StatelessWidget {
|
||||
const GettingStartedCards({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveLayout(
|
||||
smallDeviceLayout: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GeneralInfoCard(
|
||||
title: "Edit your app",
|
||||
link: null,
|
||||
subtitle: const HighlightedText(),
|
||||
),
|
||||
GeneralInfoCard(
|
||||
title: "Head to Appwrite Cloud",
|
||||
link: "https://cloud.appwrite.io",
|
||||
subtitle: const Text(
|
||||
"Start managing your project from the Appwrite console",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF56565C),
|
||||
),
|
||||
),
|
||||
),
|
||||
GeneralInfoCard(
|
||||
title: "Explore docs",
|
||||
link: "https://appwrite.io/docs",
|
||||
subtitle: const Text(
|
||||
"Discover the full power of Appwrite by diving into our documentation",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF56565C),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
largeDeviceLayout: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: context.isExtraWideScreen ? 64 : 16.0, vertical: 16.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 16,
|
||||
children: [
|
||||
Flexible(
|
||||
child: GeneralInfoCard(
|
||||
title: "Edit your app",
|
||||
link: null,
|
||||
subtitle: const HighlightedText(),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: GeneralInfoCard(
|
||||
title: "Head to Appwrite Cloud",
|
||||
link: "https://cloud.appwrite.io",
|
||||
subtitle: const Text(
|
||||
"Start managing your project from the Appwrite console",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF56565C),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: GeneralInfoCard(
|
||||
title: "Explore docs",
|
||||
link: "https://appwrite.io/docs",
|
||||
subtitle: const Text(
|
||||
"Discover the full power of Appwrite by diving into our documentation",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF56565C),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A reusable card component that displays a title and subtitle with optional link functionality.
|
||||
class GeneralInfoCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String? link;
|
||||
final Widget subtitle;
|
||||
|
||||
const GeneralInfoCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.link,
|
||||
required this.subtitle,
|
||||
});
|
||||
|
||||
void _openLink() async {
|
||||
if (link != null && await canLaunchUrl(Uri.parse(link!))) {
|
||||
await launchUrl(Uri.parse(link!));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final double cardWidth = context.isExtraWideScreen
|
||||
? constraints.maxWidth.clamp(0, 350)
|
||||
: constraints.maxWidth;
|
||||
|
||||
return SizedBox(
|
||||
width: cardWidth,
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: const BorderSide(color: Color(0xFFEDEDF0), width: 1),
|
||||
),
|
||||
color: Colors.white,
|
||||
child: InkWell(
|
||||
onTap: link != null ? _openLink : null,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 26 / 20,
|
||||
),
|
||||
),
|
||||
if (link != null) ...[
|
||||
const Spacer(),
|
||||
const Icon(
|
||||
Icons.arrow_forward,
|
||||
size: 18,
|
||||
color: Color(0xFFD8D8DB),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
subtitle,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that displays highlighted text with a specific word or phrase styled differently.
|
||||
class HighlightedText extends StatelessWidget {
|
||||
const HighlightedText({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF56565C),
|
||||
),
|
||||
children: [
|
||||
const TextSpan(text: "Edit "),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEDEDF0),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
child: const Text(
|
||||
" lib/main.dart ",
|
||||
style: TextStyle(color: Colors.black),
|
||||
),
|
||||
),
|
||||
),
|
||||
const TextSpan(text: " to get started with building your app"),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
38
lib/ui/components/responsive_layout.dart
Normal file
38
lib/ui/components/responsive_layout.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A widget that provides a responsive layout by switching between two layouts
|
||||
/// based on the available screen width.
|
||||
///
|
||||
/// This widget uses a [LayoutBuilder] to determine if the screen width exceeds
|
||||
/// the given [breakPoint]. If it does, the [largeDeviceLayout] is displayed;
|
||||
/// otherwise, the [smallDeviceLayout] is shown.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// ResponsiveLayout(
|
||||
/// breakPoint: 768,
|
||||
/// smallDeviceLayout: SmallScreenWidget(),
|
||||
/// largeDeviceLayout: LargeScreenWidget(),
|
||||
/// )
|
||||
/// ```
|
||||
class ResponsiveLayout extends StatelessWidget {
|
||||
final int breakPoint;
|
||||
final Widget smallDeviceLayout;
|
||||
final Widget largeDeviceLayout;
|
||||
|
||||
/// Creates a [ResponsiveLayout] widget.
|
||||
const ResponsiveLayout({
|
||||
super.key,
|
||||
this.breakPoint = 1024,
|
||||
required this.smallDeviceLayout,
|
||||
required this.largeDeviceLayout,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
bool isRunningOnALargeDevice = constraints.maxWidth > breakPoint;
|
||||
return isRunningOnALargeDevice ? largeDeviceLayout : smallDeviceLayout;
|
||||
});
|
||||
}
|
||||
}
|
||||
14
lib/ui/components/single_wrap.dart
Normal file
14
lib/ui/components/single_wrap.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SingleWrap extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const SingleWrap({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Wrap(
|
||||
children: [child],
|
||||
);
|
||||
}
|
||||
}
|
||||
98
lib/ui/components/top_platform_view.dart
Normal file
98
lib/ui/components/top_platform_view.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:appwrite_flutter_starter_kit/data/models/status.dart';
|
||||
import 'package:appwrite_flutter_starter_kit/ui/icons/appwrite.dart';
|
||||
import 'package:appwrite_flutter_starter_kit/utils/extensions/build_context.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'connection_line.dart';
|
||||
|
||||
/// A widget that displays a row containing platform icons and a connection line between them.
|
||||
/// The connection line indicates the status of the platform connection.
|
||||
///
|
||||
/// [status] - A boolean indicating whether the connection is successful.
|
||||
class TopPlatformView extends StatelessWidget {
|
||||
final Status status;
|
||||
|
||||
const TopPlatformView({
|
||||
super.key,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
PlatformIcon(
|
||||
size: context.isExtraWideScreen ? 142 : 100,
|
||||
child: FlutterLogo(size: context.isExtraWideScreen ? 56 : 40),
|
||||
),
|
||||
ConnectionLine(show: status == Status.success),
|
||||
PlatformIcon(
|
||||
size: context.isExtraWideScreen ? 142 : 100,
|
||||
child: AppwriteIcon(size: context.isExtraWideScreen ? 56 : 40),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that displays a stylized platform icon with a customizable content block.
|
||||
/// The icon is rendered with shadows, rounded corners, and a layered background.
|
||||
///
|
||||
/// [size] - The size of the platform icon.
|
||||
/// [child] - The inner content of the icon (e.g., an image or icon).
|
||||
class PlatformIcon extends StatelessWidget {
|
||||
final double size;
|
||||
final Widget child;
|
||||
|
||||
const PlatformIcon({
|
||||
super.key,
|
||||
this.size = 100.0,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFAFD),
|
||||
borderRadius: BorderRadius.circular(context.isExtraWideScreen ? size * 0.2 : 24),
|
||||
border: Border.all(color: const Color(0x0A19191C), width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0x08000000),
|
||||
blurRadius: 9.36,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: size * 0.86,
|
||||
height: size * 0.86,
|
||||
margin: context.isExtraWideScreen ? EdgeInsets.all(8) : null,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(context.isExtraWideScreen ? size * 0.2: 16),
|
||||
border: Border.all(color: const Color(0xFFFAFAFB), width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0x05000000),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
BoxShadow(
|
||||
color: const Color(0x05000000),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(child: child),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
178
lib/ui/icons/appwrite.dart
Normal file
178
lib/ui/icons/appwrite.dart
Normal file
@@ -0,0 +1,178 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppwriteIcon extends StatelessWidget {
|
||||
final double size; // Desired width, height scales accordingly
|
||||
final Color color;
|
||||
|
||||
const AppwriteIcon({
|
||||
super.key,
|
||||
this.size = 40,
|
||||
this.color = const Color(0xFFFD366E),
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size * (105 / 112),
|
||||
child: CustomPaint(
|
||||
painter: _AppwriteIconPainter(color),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AppwriteIconPainter extends CustomPainter {
|
||||
final Color color;
|
||||
|
||||
_AppwriteIconPainter(this.color);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final path1 = Path()
|
||||
..moveTo(size.width * 0.992, size.height * 0.75)
|
||||
..lineTo(size.width * 0.992, size.height)
|
||||
..lineTo(size.width * 0.436, size.height)
|
||||
..cubicTo(
|
||||
size.width * 0.274,
|
||||
size.height,
|
||||
size.width * 0.133,
|
||||
size.height * 0.9,
|
||||
size.width * 0.058,
|
||||
size.height * 0.75,
|
||||
)
|
||||
..cubicTo(
|
||||
size.width * 0.047,
|
||||
size.height * 0.72,
|
||||
size.width * 0.037,
|
||||
size.height * 0.69,
|
||||
size.width * 0.029,
|
||||
size.height * 0.66,
|
||||
)
|
||||
..cubicTo(
|
||||
size.width * 0.013,
|
||||
size.height * 0.62,
|
||||
size.width * 0.004,
|
||||
size.height * 0.58,
|
||||
0,
|
||||
size.height * 0.53,
|
||||
)
|
||||
..lineTo(0, size.height * 0.47)
|
||||
..cubicTo(
|
||||
0.001,
|
||||
size.height * 0.46,
|
||||
0.002,
|
||||
size.height * 0.44,
|
||||
0.003,
|
||||
size.height * 0.43,
|
||||
)
|
||||
..cubicTo(
|
||||
0.006,
|
||||
size.height * 0.41,
|
||||
0.01,
|
||||
size.height * 0.39,
|
||||
0.014,
|
||||
size.height * 0.36,
|
||||
)
|
||||
..cubicTo(
|
||||
size.width * 0.067,
|
||||
size.height * 0.15,
|
||||
size.width * 0.236,
|
||||
0,
|
||||
size.width * 0.436,
|
||||
0,
|
||||
)
|
||||
..cubicTo(
|
||||
size.width * 0.637,
|
||||
0,
|
||||
size.width * 0.805,
|
||||
size.height * 0.15,
|
||||
size.width * 0.857,
|
||||
size.height * 0.36,
|
||||
)
|
||||
..lineTo(size.width * 0.62, size.height * 0.36)
|
||||
..cubicTo(
|
||||
size.width * 0.58,
|
||||
size.height * 0.3,
|
||||
size.width * 0.514,
|
||||
size.height * 0.25,
|
||||
size.width * 0.436,
|
||||
size.height * 0.25,
|
||||
)
|
||||
..cubicTo(
|
||||
size.width * 0.357,
|
||||
size.height * 0.25,
|
||||
size.width * 0.29,
|
||||
size.height * 0.3,
|
||||
size.width * 0.251,
|
||||
size.height * 0.36,
|
||||
)
|
||||
..cubicTo(
|
||||
size.width * 0.241,
|
||||
size.height * 0.38,
|
||||
size.width * 0.232,
|
||||
size.height * 0.41,
|
||||
size.width * 0.227,
|
||||
size.height * 0.43,
|
||||
)
|
||||
..cubicTo(
|
||||
size.width * 0.221,
|
||||
size.height * 0.45,
|
||||
size.width * 0.218,
|
||||
size.height * 0.47,
|
||||
size.width * 0.218,
|
||||
size.height * 0.5,
|
||||
)
|
||||
..cubicTo(
|
||||
size.width * 0.218,
|
||||
size.height * 0.57,
|
||||
size.width * 0.244,
|
||||
size.height * 0.63,
|
||||
size.width * 0.277,
|
||||
size.height * 0.66,
|
||||
)
|
||||
..cubicTo(
|
||||
size.width * 0.316,
|
||||
size.height * 0.71,
|
||||
size.width * 0.374,
|
||||
size.height * 0.75,
|
||||
size.width * 0.436,
|
||||
size.height * 0.75,
|
||||
)
|
||||
..lineTo(size.width * 0.992, size.height * 0.75)
|
||||
..close();
|
||||
|
||||
final path2 = Path()
|
||||
..moveTo(size.width * 0.992, size.height * 0.43)
|
||||
..lineTo(size.width * 0.992, size.height * 0.66)
|
||||
..lineTo(size.width * 0.586, size.height * 0.66)
|
||||
..cubicTo(
|
||||
size.width * 0.63,
|
||||
size.height * 0.63,
|
||||
size.width * 0.654,
|
||||
size.height * 0.57,
|
||||
size.width * 0.654,
|
||||
size.height * 0.5,
|
||||
)
|
||||
..cubicTo(
|
||||
size.width * 0.654,
|
||||
size.height * 0.47,
|
||||
size.width * 0.651,
|
||||
size.height * 0.45,
|
||||
size.width * 0.646,
|
||||
size.height * 0.43,
|
||||
)
|
||||
..lineTo(size.width * 0.992, size.height * 0.43)
|
||||
..close();
|
||||
|
||||
canvas.drawPath(path1, paint);
|
||||
canvas.drawPath(path2, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
Reference in New Issue
Block a user