Files
2026-06-18 15:02:28 +08:00

211 lines
5.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:shimmer/shimmer.dart';
import '../theme/spacing.dart';
class SkeletonLineCard extends StatelessWidget {
const SkeletonLineCard({
super.key,
this.lineWidths = const [0.4, 1, 0.8, 0.6],
this.height,
});
final List<double> lineWidths;
final double? height;
@override
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
return Container(
height: height,
width: double.infinity,
padding: const EdgeInsets.all(Spacing.cardPadding),
decoration: BoxDecoration(
color: cs.card,
border: Border.all(color: cs.border, width: 1),
borderRadius: BorderRadius.circular(Spacing.radiusCard),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (var i = 0; i < lineWidths.length; i++) ...[
_SkeletonLine(widthFactor: lineWidths[i]),
if (i != lineWidths.length - 1) const SizedBox(height: 10),
],
],
),
);
}
}
class SkeletonInstitutionCard extends StatelessWidget {
const SkeletonInstitutionCard({super.key});
@override
Widget build(BuildContext context) {
return const SkeletonLineCard(lineWidths: [0.4, 1, 0.6]);
}
}
class SkeletonListenCard extends StatelessWidget {
const SkeletonListenCard({super.key});
@override
Widget build(BuildContext context) {
return const SkeletonLineCard(lineWidths: [0.4, 1, 0.6]);
}
}
class SkeletonSectionCard extends StatelessWidget {
const SkeletonSectionCard({
super.key,
required this.roman,
required this.lineWidths,
this.baseColor,
this.highlightColor,
});
final String roman;
final List<double> lineWidths;
final Color? baseColor;
final Color? highlightColor;
@override
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
final brightness = ShadTheme.of(context).brightness;
final resolvedBaseColor =
baseColor ??
(brightness == Brightness.dark
? cs.secondary
: cs.border.withValues(alpha: 0.95));
final resolvedHighlightColor =
highlightColor ??
(brightness == Brightness.dark
? cs.card
: cs.secondary.withValues(alpha: 0.95));
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: Spacing.cardPadding,
vertical: 16,
),
decoration: BoxDecoration(
color: cs.card,
border: Border.all(color: cs.border, width: 1),
borderRadius: BorderRadius.circular(Spacing.radiusCard),
),
child: Shimmer.fromColors(
baseColor: resolvedBaseColor,
highlightColor: resolvedHighlightColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (var i = 0; i < lineWidths.length; i++) ...[
_SkeletonLine(
widthFactor: lineWidths[i],
height: i == 0 ? 14 : 12,
),
if (i != lineWidths.length - 1) const SizedBox(height: 10),
],
const SizedBox(height: 2),
],
),
),
);
}
}
class _SkeletonLine extends StatelessWidget {
const _SkeletonLine({required this.widthFactor, this.height = 12});
final double widthFactor;
final double height;
@override
Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme;
return FractionallySizedBox(
widthFactor: widthFactor,
child: Shimmer.fromColors(
baseColor: cs.secondary,
highlightColor: cs.muted,
child: Container(
height: height,
decoration: BoxDecoration(
color: cs.secondary,
borderRadius: BorderRadius.circular(6),
),
),
),
);
}
}
class SectionSkeletonList extends StatelessWidget {
const SectionSkeletonList({
super.key,
required this.count,
required this.lineWidthsForIndex,
this.romanLabels,
});
final int count;
final List<double> Function(int index) lineWidthsForIndex;
final List<String>? romanLabels;
@override
Widget build(BuildContext context) {
final labels =
romanLabels ??
List<String>.generate(count, (index) => _roman(index + 1));
return SliverPadding(
padding: const EdgeInsets.fromLTRB(Spacing.page, 4, Spacing.page, 24),
sliver: SliverList.separated(
itemCount: count,
separatorBuilder: (_, _) => const SizedBox(height: Spacing.cardGap),
itemBuilder: (context, index) => SkeletonSectionCard(
roman: labels[index],
lineWidths: lineWidthsForIndex(index),
),
),
);
}
}
class SectionSkeletonListView extends StatelessWidget {
const SectionSkeletonListView({
super.key,
required this.count,
required this.lineWidthsForIndex,
this.romanLabels,
});
final int count;
final List<double> Function(int index) lineWidthsForIndex;
final List<String>? romanLabels;
@override
Widget build(BuildContext context) {
final labels =
romanLabels ??
List<String>.generate(count, (index) => _roman(index + 1));
return ListView.separated(
padding: const EdgeInsets.only(bottom: 72),
itemCount: count,
separatorBuilder: (_, _) => const SizedBox(height: Spacing.cardGap),
itemBuilder: (context, index) => SkeletonSectionCard(
roman: labels[index],
lineWidths: lineWidthsForIndex(index),
),
);
}
}
String _roman(int value) {
const numerals = ['I.', 'II.', 'III.', 'IV.', 'V.', 'VI.', 'VII.', 'VIII.'];
if (value <= 0 || value > numerals.length) return '$value.';
return numerals[value - 1];
}