forked from eva/focus-flow
fix: scale timeline task card contents
This commit is contained in:
parent
f985b3ee84
commit
dacd189435
2 changed files with 324 additions and 93 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
/// Renders Task Timeline Card pieces for the FocusFlow Flutter timeline.
|
/// Renders Task Timeline Card pieces for the FocusFlow Flutter timeline.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../models/today_screen_models.dart';
|
import '../../models/today_screen_models.dart';
|
||||||
|
|
@ -22,46 +24,87 @@ class TaskTimelineCard extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colors = _CardColors.forKind(card.visualKind);
|
final colors = _CardColors.forKind(card.visualKind);
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final metrics = _CardMetrics.fromConstraints(
|
||||||
|
constraints,
|
||||||
|
kind: card.visualKind,
|
||||||
|
);
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
key: ValueKey(card.id),
|
key: ValueKey(card.id),
|
||||||
borderRadius: BorderRadius.circular(FocusFlowTokens.cardRadius),
|
borderRadius: BorderRadius.circular(metrics.cardRadius),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.fromLTRB(18, 10, 14, 10),
|
padding: metrics.padding,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colors.background,
|
color: colors.background,
|
||||||
borderRadius: BorderRadius.circular(FocusFlowTokens.cardRadius),
|
borderRadius: BorderRadius.circular(metrics.cardRadius),
|
||||||
border: Border.all(color: colors.accent, width: 1.2),
|
border: Border.all(
|
||||||
|
color: colors.accent,
|
||||||
|
width: metrics.borderWidth,
|
||||||
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: colors.accent.withValues(alpha: 0.12),
|
color: colors.accent.withValues(alpha: 0.12),
|
||||||
blurRadius: 18,
|
blurRadius: metrics.shadowBlur,
|
||||||
spreadRadius: 1,
|
spreadRadius: metrics.shadowSpread,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
_StatusRing(card: card, color: colors.accent),
|
_StatusRing(
|
||||||
const SizedBox(width: 22),
|
card: card,
|
||||||
|
color: colors.accent,
|
||||||
|
metrics: metrics,
|
||||||
|
),
|
||||||
|
SizedBox(width: metrics.statusGap),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _CardText(card: card, color: colors.accent),
|
child: _CardText(
|
||||||
|
card: card,
|
||||||
|
color: colors.accent,
|
||||||
|
metrics: metrics,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (card.showsQuickActions) ...[
|
if (card.showsQuickActions) ...[
|
||||||
_NoOpIcon(icon: Icons.check, color: colors.accent),
|
_NoOpIcon(
|
||||||
_NoOpIcon(icon: Icons.arrow_forward, color: colors.accent),
|
icon: Icons.check,
|
||||||
_NoOpIcon(icon: Icons.calendar_today, color: colors.accent),
|
color: colors.accent,
|
||||||
_NoOpIcon(icon: Icons.wb_sunny_outlined, color: colors.accent),
|
metrics: metrics,
|
||||||
const SizedBox(width: 10),
|
),
|
||||||
|
_NoOpIcon(
|
||||||
|
icon: Icons.arrow_forward,
|
||||||
|
color: colors.accent,
|
||||||
|
metrics: metrics,
|
||||||
|
),
|
||||||
|
_NoOpIcon(
|
||||||
|
icon: Icons.calendar_today,
|
||||||
|
color: colors.accent,
|
||||||
|
metrics: metrics,
|
||||||
|
),
|
||||||
|
_NoOpIcon(
|
||||||
|
icon: Icons.wb_sunny_outlined,
|
||||||
|
color: colors.accent,
|
||||||
|
metrics: metrics,
|
||||||
|
),
|
||||||
|
SizedBox(width: metrics.actionGap),
|
||||||
],
|
],
|
||||||
_Badge(child: RewardIcon(color: colors.reward, size: 26)),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
_Badge(
|
_Badge(
|
||||||
|
metrics: metrics,
|
||||||
|
child: RewardIcon(
|
||||||
|
color: colors.reward,
|
||||||
|
size: metrics.rewardIconSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: metrics.badgeGap),
|
||||||
|
_Badge(
|
||||||
|
metrics: metrics,
|
||||||
child: DifficultyBars(
|
child: DifficultyBars(
|
||||||
difficulty: card.difficultyIconToken,
|
difficulty: card.difficultyIconToken,
|
||||||
color: colors.reward,
|
color: colors.reward,
|
||||||
|
size: metrics.difficultySize,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -69,40 +112,110 @@ class TaskTimelineCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _CardMetrics {
|
||||||
|
const _CardMetrics(this.scale);
|
||||||
|
|
||||||
|
static const _referenceWidth = 640.0;
|
||||||
|
static const _referenceHeight = 88.0;
|
||||||
|
static const _freeSlotReferenceHeight = 108.0;
|
||||||
|
static const _minimumScale = 0.62;
|
||||||
|
|
||||||
|
factory _CardMetrics.fromConstraints(
|
||||||
|
BoxConstraints constraints, {
|
||||||
|
required TaskVisualKind kind,
|
||||||
|
}) {
|
||||||
|
final width = constraints.hasBoundedWidth
|
||||||
|
? constraints.maxWidth
|
||||||
|
: _referenceWidth;
|
||||||
|
final height = constraints.hasBoundedHeight
|
||||||
|
? constraints.maxHeight
|
||||||
|
: _referenceHeight;
|
||||||
|
final widthScale = width / _referenceWidth;
|
||||||
|
final heightScale =
|
||||||
|
height /
|
||||||
|
(kind == TaskVisualKind.freeSlot
|
||||||
|
? _freeSlotReferenceHeight
|
||||||
|
: _referenceHeight);
|
||||||
|
final scale = math.min(widthScale, heightScale).clamp(_minimumScale, 1.0);
|
||||||
|
return _CardMetrics(scale.toDouble());
|
||||||
|
}
|
||||||
|
|
||||||
|
final double scale;
|
||||||
|
|
||||||
|
double get cardRadius => FocusFlowTokens.cardRadius * scale;
|
||||||
|
double get borderWidth => math.max(1, 1.2 * scale);
|
||||||
|
double get shadowBlur => 18 * scale;
|
||||||
|
double get shadowSpread => scale;
|
||||||
|
double get statusRingSize => 34 * scale;
|
||||||
|
double get statusBorderWidth => math.max(1.2, 3 * scale);
|
||||||
|
double get freeSlotStatusBorderWidth => math.max(1, 2 * scale);
|
||||||
|
double get statusIconSize => 24 * scale;
|
||||||
|
double get statusGap => 22 * scale;
|
||||||
|
double get actionButtonSize => 40 * scale;
|
||||||
|
double get actionIconSize => 22 * scale;
|
||||||
|
double get actionGap => 10 * scale;
|
||||||
|
double get badgeWidth => 62 * scale;
|
||||||
|
double get badgeHeight => 48 * scale;
|
||||||
|
double get badgeRadius => 7 * scale;
|
||||||
|
double get badgeGap => 8 * scale;
|
||||||
|
double get rewardIconSize => 26 * scale;
|
||||||
|
double get titleSize => FocusFlowTokens.cardTitleSize * scale;
|
||||||
|
double get metaSize => FocusFlowTokens.cardMetaSize * scale;
|
||||||
|
double get titleMetaGap => 4 * scale;
|
||||||
|
double get freeSlotTimeGap => 10 * scale;
|
||||||
|
Size get difficultySize => Size(54 * scale, 34 * scale);
|
||||||
|
EdgeInsets get padding =>
|
||||||
|
EdgeInsets.fromLTRB(18 * scale, 10 * scale, 14 * scale, 10 * scale);
|
||||||
|
}
|
||||||
|
|
||||||
class _StatusRing extends StatelessWidget {
|
class _StatusRing extends StatelessWidget {
|
||||||
const _StatusRing({required this.card, required this.color});
|
const _StatusRing({
|
||||||
|
required this.card,
|
||||||
|
required this.color,
|
||||||
|
required this.metrics,
|
||||||
|
});
|
||||||
|
|
||||||
final TimelineCardModel card;
|
final TimelineCardModel card;
|
||||||
final Color color;
|
final Color color;
|
||||||
|
final _CardMetrics metrics;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final border = Border.all(
|
final border = Border.all(
|
||||||
color: color,
|
color: color,
|
||||||
width: card.visualKind == TaskVisualKind.freeSlot ? 2 : 3,
|
width: card.visualKind == TaskVisualKind.freeSlot
|
||||||
|
? metrics.freeSlotStatusBorderWidth
|
||||||
|
: metrics.statusBorderWidth,
|
||||||
style: card.visualKind == TaskVisualKind.freeSlot
|
style: card.visualKind == TaskVisualKind.freeSlot
|
||||||
? BorderStyle.solid
|
? BorderStyle.solid
|
||||||
: BorderStyle.solid,
|
: BorderStyle.solid,
|
||||||
);
|
);
|
||||||
return Container(
|
return Container(
|
||||||
width: 34,
|
width: metrics.statusRingSize,
|
||||||
height: 34,
|
height: metrics.statusRingSize,
|
||||||
decoration: BoxDecoration(shape: BoxShape.circle, border: border),
|
decoration: BoxDecoration(shape: BoxShape.circle, border: border),
|
||||||
child: card.isCompleted
|
child: card.isCompleted
|
||||||
? Icon(Icons.check, color: color, size: 24)
|
? Icon(Icons.check, color: color, size: metrics.statusIconSize)
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CardText extends StatelessWidget {
|
class _CardText extends StatelessWidget {
|
||||||
const _CardText({required this.card, required this.color});
|
const _CardText({
|
||||||
|
required this.card,
|
||||||
|
required this.color,
|
||||||
|
required this.metrics,
|
||||||
|
});
|
||||||
|
|
||||||
final TimelineCardModel card;
|
final TimelineCardModel card;
|
||||||
final Color color;
|
final Color color;
|
||||||
|
final _CardMetrics metrics;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -110,13 +223,16 @@ class _CardText extends StatelessWidget {
|
||||||
color: card.isCompleted
|
color: card.isCompleted
|
||||||
? FocusFlowTokens.completedText
|
? FocusFlowTokens.completedText
|
||||||
: FocusFlowTokens.textPrimary,
|
: FocusFlowTokens.textPrimary,
|
||||||
fontSize: FocusFlowTokens.cardTitleSize,
|
fontSize: metrics.titleSize,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
decoration: card.isCompleted ? TextDecoration.lineThrough : null,
|
decoration: card.isCompleted ? TextDecoration.lineThrough : null,
|
||||||
decorationColor: FocusFlowTokens.completedText,
|
decorationColor: FocusFlowTokens.completedText,
|
||||||
decorationThickness: 2,
|
decorationThickness: 2 * metrics.scale,
|
||||||
);
|
);
|
||||||
return Column(
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final textBlock = Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -126,14 +242,14 @@ class _CardText extends StatelessWidget {
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: titleStyle,
|
style: titleStyle,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
SizedBox(height: metrics.titleMetaGap),
|
||||||
Text(
|
Text(
|
||||||
card.subtitle,
|
card.subtitle,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: card.isCompleted ? FocusFlowTokens.completedText : color,
|
color: card.isCompleted ? FocusFlowTokens.completedText : color,
|
||||||
fontSize: FocusFlowTokens.cardMetaSize,
|
fontSize: metrics.metaSize,
|
||||||
fontStyle: card.visualKind == TaskVisualKind.freeSlot
|
fontStyle: card.visualKind == TaskVisualKind.freeSlot
|
||||||
? FontStyle.italic
|
? FontStyle.italic
|
||||||
: FontStyle.normal,
|
: FontStyle.normal,
|
||||||
|
|
@ -143,52 +259,75 @@ class _CardText extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (card.visualKind == TaskVisualKind.freeSlot) ...[
|
if (card.visualKind == TaskVisualKind.freeSlot) ...[
|
||||||
const SizedBox(height: 10),
|
SizedBox(height: metrics.freeSlotTimeGap),
|
||||||
Text(
|
Text(
|
||||||
card.timeText,
|
card.timeText,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: color,
|
color: color,
|
||||||
fontSize: FocusFlowTokens.cardMetaSize,
|
fontSize: metrics.metaSize,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
if (!constraints.hasBoundedWidth || !constraints.hasBoundedHeight) {
|
||||||
|
return textBlock;
|
||||||
|
}
|
||||||
|
return FittedBox(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: SizedBox(width: constraints.maxWidth, child: textBlock),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NoOpIcon extends StatelessWidget {
|
class _NoOpIcon extends StatelessWidget {
|
||||||
const _NoOpIcon({required this.icon, required this.color});
|
const _NoOpIcon({
|
||||||
|
required this.icon,
|
||||||
|
required this.color,
|
||||||
|
required this.metrics,
|
||||||
|
});
|
||||||
|
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final Color color;
|
final Color color;
|
||||||
|
final _CardMetrics metrics;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () {},
|
onPressed: () {},
|
||||||
|
constraints: BoxConstraints.tightFor(
|
||||||
|
width: metrics.actionButtonSize,
|
||||||
|
height: metrics.actionButtonSize,
|
||||||
|
),
|
||||||
color: color,
|
color: color,
|
||||||
|
iconSize: metrics.actionIconSize,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
icon: Icon(icon),
|
icon: Icon(icon),
|
||||||
tooltip: 'No-op action',
|
tooltip: 'No-op action',
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Badge extends StatelessWidget {
|
class _Badge extends StatelessWidget {
|
||||||
const _Badge({required this.child});
|
const _Badge({required this.child, required this.metrics});
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
final _CardMetrics metrics;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
width: 62,
|
width: metrics.badgeWidth,
|
||||||
height: 48,
|
height: metrics.badgeHeight,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: FocusFlowTokens.panelBackground.withValues(alpha: 0.84),
|
color: FocusFlowTokens.panelBackground.withValues(alpha: 0.84),
|
||||||
borderRadius: BorderRadius.circular(7),
|
borderRadius: BorderRadius.circular(metrics.badgeRadius),
|
||||||
border: Border.all(color: FocusFlowTokens.subtleBorder),
|
border: Border.all(color: FocusFlowTokens.subtleBorder),
|
||||||
),
|
),
|
||||||
child: child,
|
child: child,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import 'package:focus_flow_flutter/app/demo_scheduler_composition.dart';
|
||||||
import 'package:focus_flow_flutter/app/focus_flow_app.dart';
|
import 'package:focus_flow_flutter/app/focus_flow_app.dart';
|
||||||
import 'package:focus_flow_flutter/models/today_screen_models.dart';
|
import 'package:focus_flow_flutter/models/today_screen_models.dart';
|
||||||
import 'package:focus_flow_flutter/widgets/timeline/difficulty_bars.dart';
|
import 'package:focus_flow_flutter/widgets/timeline/difficulty_bars.dart';
|
||||||
|
import 'package:focus_flow_flutter/widgets/timeline/task_timeline_card.dart';
|
||||||
|
|
||||||
/// Runs FocusFlow widget and visual adapter tests.
|
/// Runs FocusFlow widget and visual adapter tests.
|
||||||
void main() {
|
void main() {
|
||||||
|
|
@ -109,6 +110,48 @@ void main() {
|
||||||
expect(find.byKey(const ValueKey('task-selection-modal')), findsNothing);
|
expect(find.byKey(const ValueKey('task-selection-modal')), findsNothing);
|
||||||
expect(find.text('Pay bill'), findsOneWidget);
|
expect(find.text('Pay bill'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('task card controls scale inside a narrow task bubble', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await _pumpTimelineCard(
|
||||||
|
tester,
|
||||||
|
card: _timelineCard(
|
||||||
|
id: 'narrow-actions',
|
||||||
|
title: 'A task title that would otherwise crowd the controls',
|
||||||
|
),
|
||||||
|
size: const Size(320, 88),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
|
||||||
|
final button = tester.widget<IconButton>(
|
||||||
|
find.widgetWithIcon(IconButton, Icons.arrow_forward),
|
||||||
|
);
|
||||||
|
expect(button.iconSize, lessThan(18));
|
||||||
|
expect(button.constraints?.maxWidth, lessThan(32));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('free slot text scales inside a short task bubble', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await _pumpTimelineCard(
|
||||||
|
tester,
|
||||||
|
card: _timelineCard(
|
||||||
|
id: 'short-free-slot',
|
||||||
|
title: 'Free Slot',
|
||||||
|
subtitle: 'Intentional rest',
|
||||||
|
timeText: '6:15 PM - 6:30 PM',
|
||||||
|
visualKind: TaskVisualKind.freeSlot,
|
||||||
|
showsQuickActions: false,
|
||||||
|
),
|
||||||
|
size: const Size(360, 88),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
expect(find.text('Intentional rest'), findsOneWidget);
|
||||||
|
expect(find.text('6:15 PM - 6:30 PM'), findsOneWidget);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pumpPlanApp(WidgetTester tester) async {
|
Future<void> _pumpPlanApp(WidgetTester tester) async {
|
||||||
|
|
@ -136,3 +179,52 @@ Future<TodayState> _readSeededToday() async {
|
||||||
TaskVisualKind _kindFor(TodayScreenData data, String id) {
|
TaskVisualKind _kindFor(TodayScreenData data, String id) {
|
||||||
return data.cards.singleWhere((card) => card.id == id).visualKind;
|
return data.cards.singleWhere((card) => card.id == id).visualKind;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _pumpTimelineCard(
|
||||||
|
WidgetTester tester, {
|
||||||
|
required TimelineCardModel card,
|
||||||
|
required Size size,
|
||||||
|
}) async {
|
||||||
|
await tester.binding.setSurfaceSize(Size(size.width + 40, size.height + 40));
|
||||||
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: size.width,
|
||||||
|
height: size.height,
|
||||||
|
child: TaskTimelineCard(card: card, onTap: () {}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
TimelineCardModel _timelineCard({
|
||||||
|
required String id,
|
||||||
|
required String title,
|
||||||
|
String subtitle = '30 min',
|
||||||
|
String timeText = '6:00 PM - 6:30 PM',
|
||||||
|
TaskVisualKind visualKind = TaskVisualKind.flexible,
|
||||||
|
bool showsQuickActions = true,
|
||||||
|
}) {
|
||||||
|
return TimelineCardModel(
|
||||||
|
id: id,
|
||||||
|
title: title,
|
||||||
|
typeLabel: 'Flexible',
|
||||||
|
subtitle: subtitle,
|
||||||
|
timeText: timeText,
|
||||||
|
startMinutes: 18 * 60,
|
||||||
|
endMinutes: 18 * 60 + 30,
|
||||||
|
visualKind: visualKind,
|
||||||
|
rewardIconToken: TimelineRewardIconToken.medium,
|
||||||
|
difficultyIconToken: TimelineDifficultyIconToken.medium,
|
||||||
|
isCompleted: false,
|
||||||
|
isSelectable: true,
|
||||||
|
showsQuickActions: showsQuickActions,
|
||||||
|
durationMinutes: 30,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue