This commit is contained in:
13
lib/presentation/plan_editor/models/exercise_drag_data.dart
Normal file
13
lib/presentation/plan_editor/models/exercise_drag_data.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:trainhub_flutter/domain/entities/training_exercise.dart';
|
||||
|
||||
class ExerciseDragData {
|
||||
const ExerciseDragData({
|
||||
required this.fromSectionIndex,
|
||||
required this.exerciseIndex,
|
||||
required this.exercise,
|
||||
});
|
||||
|
||||
final int fromSectionIndex;
|
||||
final int exerciseIndex;
|
||||
final TrainingExerciseEntity exercise;
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import 'package:trainhub_flutter/core/utils/id_generator.dart';
|
||||
import 'package:trainhub_flutter/injection.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/exercise.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/training_exercise.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/training_plan.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/training_section.dart';
|
||||
import 'package:trainhub_flutter/domain/repositories/exercise_repository.dart';
|
||||
import 'package:trainhub_flutter/domain/repositories/training_plan_repository.dart';
|
||||
@@ -14,14 +13,15 @@ part 'plan_editor_controller.g.dart';
|
||||
@riverpod
|
||||
class PlanEditorController extends _$PlanEditorController {
|
||||
late TrainingPlanRepository _planRepo;
|
||||
late ExerciseRepository _exerciseRepo;
|
||||
|
||||
@override
|
||||
Future<PlanEditorState> build(String planId) async {
|
||||
_planRepo = getIt<TrainingPlanRepository>();
|
||||
final ExerciseRepository exerciseRepo = getIt<ExerciseRepository>();
|
||||
_exerciseRepo = getIt<ExerciseRepository>();
|
||||
|
||||
final plan = await _planRepo.getById(planId);
|
||||
final exercises = await exerciseRepo.getAll();
|
||||
final exercises = await _exerciseRepo.getAll();
|
||||
|
||||
return PlanEditorState(plan: plan, availableExercises: exercises);
|
||||
}
|
||||
@@ -64,6 +64,21 @@ class PlanEditorController extends _$PlanEditorController {
|
||||
);
|
||||
}
|
||||
|
||||
void reorderSection(int oldIndex, int newIndex) {
|
||||
final current = state.valueOrNull;
|
||||
if (current == null) return;
|
||||
final sections = List<TrainingSectionEntity>.from(current.plan.sections);
|
||||
if (oldIndex < newIndex) newIndex -= 1;
|
||||
final item = sections.removeAt(oldIndex);
|
||||
sections.insert(newIndex, item);
|
||||
state = AsyncValue.data(
|
||||
current.copyWith(
|
||||
plan: current.plan.copyWith(sections: sections),
|
||||
isDirty: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void updateSectionName(int sectionIndex, String name) {
|
||||
final current = state.valueOrNull;
|
||||
if (current == null) return;
|
||||
@@ -116,6 +131,37 @@ class PlanEditorController extends _$PlanEditorController {
|
||||
);
|
||||
}
|
||||
|
||||
void moveExerciseBetweenSections({
|
||||
required int fromSectionIndex,
|
||||
required int exerciseIndex,
|
||||
required int toSectionIndex,
|
||||
}) {
|
||||
final current = state.valueOrNull;
|
||||
if (current == null) return;
|
||||
if (fromSectionIndex == toSectionIndex) return;
|
||||
final sections = List<TrainingSectionEntity>.from(current.plan.sections);
|
||||
final fromExercises = List<TrainingExerciseEntity>.from(
|
||||
sections[fromSectionIndex].exercises,
|
||||
);
|
||||
final toExercises = List<TrainingExerciseEntity>.from(
|
||||
sections[toSectionIndex].exercises,
|
||||
);
|
||||
final exercise = fromExercises.removeAt(exerciseIndex);
|
||||
toExercises.add(exercise);
|
||||
sections[fromSectionIndex] = sections[fromSectionIndex].copyWith(
|
||||
exercises: fromExercises,
|
||||
);
|
||||
sections[toSectionIndex] = sections[toSectionIndex].copyWith(
|
||||
exercises: toExercises,
|
||||
);
|
||||
state = AsyncValue.data(
|
||||
current.copyWith(
|
||||
plan: current.plan.copyWith(sections: sections),
|
||||
isDirty: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void updateExerciseParams(
|
||||
int sectionIndex,
|
||||
int exerciseIndex, {
|
||||
@@ -168,6 +214,25 @@ class PlanEditorController extends _$PlanEditorController {
|
||||
);
|
||||
}
|
||||
|
||||
Future<ExerciseEntity> createExercise({
|
||||
required String name,
|
||||
String? instructions,
|
||||
String? tags,
|
||||
String? videoUrl,
|
||||
}) async {
|
||||
final current = state.valueOrNull;
|
||||
if (current == null) throw StateError('Controller state not loaded');
|
||||
final exercise = await _exerciseRepo.create(
|
||||
name: name,
|
||||
instructions: instructions,
|
||||
tags: tags,
|
||||
videoUrl: videoUrl,
|
||||
);
|
||||
final exercises = await _exerciseRepo.getAll();
|
||||
state = AsyncValue.data(current.copyWith(availableExercises: exercises));
|
||||
return exercise;
|
||||
}
|
||||
|
||||
Future<void> save() async {
|
||||
final current = state.valueOrNull;
|
||||
if (current == null) return;
|
||||
|
||||
@@ -7,7 +7,7 @@ part of 'plan_editor_controller.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$planEditorControllerHash() =>
|
||||
r'4045493829126f28b3a58695b68ade53519c1412';
|
||||
r'6c6c2f74725e250bd41401cab12c1a62306d10ea';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
|
||||
import 'package:trainhub_flutter/core/theme/app_colors.dart';
|
||||
import 'package:trainhub_flutter/presentation/plan_editor/plan_editor_controller.dart';
|
||||
import 'package:trainhub_flutter/presentation/plan_editor/widgets/plan_section_card.dart';
|
||||
|
||||
@@ -16,64 +19,182 @@ class PlanEditorPage extends ConsumerWidget {
|
||||
final controller = ref.read(planEditorControllerProvider(planId).notifier);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: state.when(
|
||||
data: (data) => TextField(
|
||||
controller: TextEditingController(text: data.plan.name)
|
||||
..selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: data.plan.name.length),
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: 'Plan Name',
|
||||
backgroundColor: AppColors.surface,
|
||||
body: Column(
|
||||
children: [
|
||||
// --- Custom header bar ---
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.surfaceContainer,
|
||||
border: Border(bottom: BorderSide(color: AppColors.border)),
|
||||
),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
onChanged: controller.updatePlanName,
|
||||
),
|
||||
error: (_, __) => const Text('Error'),
|
||||
loading: () => const Text('Loading...'),
|
||||
),
|
||||
actions: [
|
||||
state.maybeWhen(
|
||||
data: (data) => IconButton(
|
||||
icon: Icon(
|
||||
Icons.save,
|
||||
color: data.isDirty ? Theme.of(context).primaryColor : null,
|
||||
),
|
||||
onPressed: data.isDirty ? () => controller.save() : null,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing24,
|
||||
vertical: UIConstants.spacing12,
|
||||
),
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: state.when(
|
||||
data: (data) => ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: data.plan.sections.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == data.plan.sections.length) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: controller.addSection,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add Section'),
|
||||
child: Row(
|
||||
children: [
|
||||
// Back button
|
||||
Material(
|
||||
color: AppColors.zinc800,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InkWell(
|
||||
onTap: () => context.router.maybePop(),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: Icon(
|
||||
Icons.arrow_back,
|
||||
color: AppColors.textSecondary,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final section = data.plan.sections[index];
|
||||
return PlanSectionCard(
|
||||
section: section,
|
||||
sectionIndex: index,
|
||||
plan: data.plan,
|
||||
availableExercises: data.availableExercises,
|
||||
);
|
||||
},
|
||||
),
|
||||
error: (e, s) => Center(child: Text('Error: $e')),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
const SizedBox(width: UIConstants.spacing16),
|
||||
|
||||
// Plan icon badge
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.accentMuted,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.description_outlined,
|
||||
color: AppColors.accent,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing12),
|
||||
|
||||
// Plan name text field
|
||||
Expanded(
|
||||
child: state.maybeWhen(
|
||||
data: (data) => TextField(
|
||||
controller: TextEditingController(text: data.plan.name)
|
||||
..selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: data.plan.name.length),
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
filled: false,
|
||||
hintText: 'Untitled Plan',
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
letterSpacing: -0.3,
|
||||
),
|
||||
onChanged: controller.updatePlanName,
|
||||
),
|
||||
orElse: () => Text(
|
||||
'Loading...',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Unsaved changes badge + save button
|
||||
state.maybeWhen(
|
||||
data: (data) => data.isDirty
|
||||
? Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing12,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.warning.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: AppColors.warning.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Unsaved changes',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.warning,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing12),
|
||||
FilledButton.icon(
|
||||
onPressed: controller.save,
|
||||
icon: const Icon(Icons.save_outlined, size: 16),
|
||||
label: const Text('Save'),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// --- Body ---
|
||||
Expanded(
|
||||
child: state.when(
|
||||
data: (data) => ReorderableListView.builder(
|
||||
padding: const EdgeInsets.all(UIConstants.spacing24),
|
||||
onReorder: controller.reorderSection,
|
||||
footer: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UIConstants.spacing16,
|
||||
),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: controller.addSection,
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('Add Section'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.textSecondary,
|
||||
side: const BorderSide(color: AppColors.border),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing24,
|
||||
vertical: UIConstants.spacing12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
itemCount: data.plan.sections.length,
|
||||
itemBuilder: (context, index) {
|
||||
final section = data.plan.sections[index];
|
||||
return PlanSectionCard(
|
||||
key: ValueKey(section.id),
|
||||
section: section,
|
||||
sectionIndex: index,
|
||||
plan: data.plan,
|
||||
availableExercises: data.availableExercises,
|
||||
);
|
||||
},
|
||||
),
|
||||
error: (e, s) => Center(
|
||||
child: Text(
|
||||
'Error: $e',
|
||||
style: const TextStyle(color: AppColors.destructive),
|
||||
),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
|
||||
import 'package:trainhub_flutter/core/theme/app_colors.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/training_exercise.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/training_plan.dart';
|
||||
import 'package:trainhub_flutter/presentation/plan_editor/models/exercise_drag_data.dart';
|
||||
import 'package:trainhub_flutter/presentation/plan_editor/plan_editor_controller.dart';
|
||||
|
||||
class PlanExerciseTile extends ConsumerWidget {
|
||||
class PlanExerciseTile extends ConsumerStatefulWidget {
|
||||
final TrainingExerciseEntity exercise;
|
||||
final int sectionIndex;
|
||||
final int exerciseIndex;
|
||||
@@ -19,100 +23,192 @@ class PlanExerciseTile extends ConsumerWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final controller = ref.read(planEditorControllerProvider(plan.id).notifier);
|
||||
ConsumerState<PlanExerciseTile> createState() => _PlanExerciseTileState();
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
elevation: 0,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(color: Theme.of(context).colorScheme.outlineVariant),
|
||||
class _PlanExerciseTileState extends ConsumerState<PlanExerciseTile> {
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = ref.read(
|
||||
planEditorControllerProvider(widget.plan.id).notifier,
|
||||
);
|
||||
final dragData = ExerciseDragData(
|
||||
fromSectionIndex: widget.sectionIndex,
|
||||
exerciseIndex: widget.exerciseIndex,
|
||||
exercise: widget.exercise,
|
||||
);
|
||||
|
||||
return LongPressDraggable<ExerciseDragData>(
|
||||
data: dragData,
|
||||
feedback: Material(
|
||||
elevation: 8,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
width: 220,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.zinc800,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.accent.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.drag_indicator,
|
||||
size: 15,
|
||||
color: AppColors.accent,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.exercise.name,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
childWhenDragging: Opacity(
|
||||
opacity: 0.35,
|
||||
child: _buildContent(controller),
|
||||
),
|
||||
child: MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
onExit: (_) => setState(() => _isHovered = false),
|
||||
child: _buildContent(controller),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(PlanEditorController controller) {
|
||||
return AnimatedContainer(
|
||||
duration: UIConstants.animationDuration,
|
||||
margin: const EdgeInsets.symmetric(vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: _isHovered ? AppColors.zinc800 : AppColors.zinc900,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: _isHovered ? AppColors.zinc700 : AppColors.border,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
// --- Header: drag handle + name + delete ---
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.drag_handle,
|
||||
size: 15,
|
||||
color: AppColors.zinc600,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
exercise.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
widget.exercise.name,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () => controller.removeExerciseFromSection(
|
||||
sectionIndex,
|
||||
exerciseIndex,
|
||||
AnimatedOpacity(
|
||||
opacity: _isHovered ? 1.0 : 0.0,
|
||||
duration: UIConstants.animationDuration,
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.removeExerciseFromSection(
|
||||
widget.sectionIndex,
|
||||
widget.exerciseIndex,
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(2),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 14,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// --- Fields row ---
|
||||
Row(
|
||||
children: [
|
||||
_buildNumberInput(
|
||||
context,
|
||||
_FieldBox(
|
||||
label: 'Sets',
|
||||
value: exercise.sets,
|
||||
value: widget.exercise.sets,
|
||||
onChanged: (val) => controller.updateExerciseParams(
|
||||
sectionIndex,
|
||||
exerciseIndex,
|
||||
widget.sectionIndex,
|
||||
widget.exerciseIndex,
|
||||
sets: val,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildNumberInput(
|
||||
context,
|
||||
label: exercise.isTime ? 'Secs' : 'Reps',
|
||||
value: exercise.value,
|
||||
_FieldBox(
|
||||
label: widget.exercise.isTime ? 'Secs' : 'Reps',
|
||||
value: widget.exercise.value,
|
||||
onChanged: (val) => controller.updateExerciseParams(
|
||||
sectionIndex,
|
||||
exerciseIndex,
|
||||
widget.sectionIndex,
|
||||
widget.exerciseIndex,
|
||||
value: val,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildNumberInput(
|
||||
context,
|
||||
_FieldBox(
|
||||
label: 'Rest(s)',
|
||||
value: exercise.rest,
|
||||
value: widget.exercise.rest,
|
||||
onChanged: (val) => controller.updateExerciseParams(
|
||||
sectionIndex,
|
||||
exerciseIndex,
|
||||
widget.sectionIndex,
|
||||
widget.exerciseIndex,
|
||||
rest: val,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Toggle Time/Reps
|
||||
InkWell(
|
||||
// Time toggle pill
|
||||
GestureDetector(
|
||||
onTap: () => controller.updateExerciseParams(
|
||||
sectionIndex,
|
||||
exerciseIndex,
|
||||
isTime: !exercise.isTime,
|
||||
widget.sectionIndex,
|
||||
widget.exerciseIndex,
|
||||
isTime: !widget.exercise.isTime,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: AnimatedContainer(
|
||||
duration: UIConstants.animationDuration,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.exercise.isTime
|
||||
? AppColors.accentMuted
|
||||
: AppColors.zinc800,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
color: widget.exercise.isTime
|
||||
? AppColors.accent.withValues(alpha: 0.4)
|
||||
: AppColors.border,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: exercise.isTime
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: null,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.timer,
|
||||
size: 16,
|
||||
color: exercise.isTime
|
||||
? Theme.of(context).colorScheme.onPrimaryContainer
|
||||
: null,
|
||||
Icons.timer_outlined,
|
||||
size: 15,
|
||||
color: widget.exercise.isTime
|
||||
? AppColors.accent
|
||||
: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -123,38 +219,75 @@ class PlanExerciseTile extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildNumberInput(
|
||||
BuildContext context, {
|
||||
required String label,
|
||||
required int value,
|
||||
required Function(int) onChanged,
|
||||
}) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compact field box (label + number input)
|
||||
// ---------------------------------------------------------------------------
|
||||
class _FieldBox extends StatelessWidget {
|
||||
final String label;
|
||||
final int value;
|
||||
final Function(int) onChanged;
|
||||
|
||||
const _FieldBox({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: Theme.of(context).textTheme.labelSmall),
|
||||
Text(
|
||||
label,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textMuted,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
height: 32,
|
||||
child: TextField(
|
||||
controller: TextEditingController(text: value.toString())
|
||||
..selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: value.toString().length),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: AppColors.zinc950,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 0,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: const BorderSide(color: AppColors.border),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: const BorderSide(color: AppColors.border),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
borderSide: const BorderSide(color: AppColors.zinc500),
|
||||
),
|
||||
),
|
||||
onChanged: (val) {
|
||||
final intVal = int.tryParse(val);
|
||||
if (intVal != null) {
|
||||
onChanged(intVal);
|
||||
}
|
||||
if (intVal != null) onChanged(intVal);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
|
||||
import 'package:trainhub_flutter/core/theme/app_colors.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/exercise.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/training_plan.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/training_section.dart';
|
||||
import 'package:trainhub_flutter/presentation/plan_editor/models/exercise_drag_data.dart';
|
||||
import 'package:trainhub_flutter/presentation/plan_editor/plan_editor_controller.dart';
|
||||
import 'package:trainhub_flutter/presentation/plan_editor/widgets/plan_exercise_tile.dart';
|
||||
|
||||
class PlanSectionCard extends ConsumerWidget {
|
||||
class PlanSectionCard extends ConsumerStatefulWidget {
|
||||
final TrainingSectionEntity section;
|
||||
final int sectionIndex;
|
||||
final TrainingPlanEntity plan;
|
||||
@@ -21,73 +25,251 @@ class PlanSectionCard extends ConsumerWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final controller = ref.read(planEditorControllerProvider(plan.id).notifier);
|
||||
ConsumerState<PlanSectionCard> createState() => _PlanSectionCardState();
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
class _PlanSectionCardState extends ConsumerState<PlanSectionCard> {
|
||||
bool _isDragOver = false;
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = ref.read(
|
||||
planEditorControllerProvider(widget.plan.id).notifier,
|
||||
);
|
||||
|
||||
return DragTarget<ExerciseDragData>(
|
||||
onWillAcceptWithDetails: (details) =>
|
||||
details.data.fromSectionIndex != widget.sectionIndex,
|
||||
onAcceptWithDetails: (details) {
|
||||
controller.moveExerciseBetweenSections(
|
||||
fromSectionIndex: details.data.fromSectionIndex,
|
||||
exerciseIndex: details.data.exerciseIndex,
|
||||
toSectionIndex: widget.sectionIndex,
|
||||
);
|
||||
setState(() => _isDragOver = false);
|
||||
},
|
||||
onLeave: (_) => setState(() => _isDragOver = false),
|
||||
onMove: (_) => setState(() => _isDragOver = true),
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
onExit: (_) => setState(() => _isHovered = false),
|
||||
child: AnimatedContainer(
|
||||
duration: UIConstants.animationDuration,
|
||||
margin: const EdgeInsets.only(bottom: UIConstants.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: _isDragOver ? AppColors.accent : AppColors.border,
|
||||
width: _isDragOver ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: TextEditingController(text: section.name)
|
||||
..selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: section.name.length),
|
||||
// --- Section header ---
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
UIConstants.spacing16,
|
||||
UIConstants.spacing12,
|
||||
UIConstants.spacing8,
|
||||
UIConstants.spacing12,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.drag_handle,
|
||||
color: AppColors.zinc600,
|
||||
size: 18,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: 'Section Name',
|
||||
isDense: true,
|
||||
),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
onChanged: (val) =>
|
||||
controller.updateSectionName(sectionIndex, val),
|
||||
const SizedBox(width: UIConstants.spacing8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: TextEditingController(
|
||||
text: widget.section.name,
|
||||
)..selection = TextSelection.fromPosition(
|
||||
TextPosition(
|
||||
offset: widget.section.name.length,
|
||||
),
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
filled: false,
|
||||
hintText: 'Section name',
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
onChanged: (val) => controller.updateSectionName(
|
||||
widget.sectionIndex,
|
||||
val,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Exercise count badge
|
||||
if (widget.section.exercises.isNotEmpty)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(
|
||||
right: UIConstants.spacing8,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.zinc800,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
'${widget.section.exercises.length}',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Delete button — shows on hover
|
||||
AnimatedOpacity(
|
||||
opacity: _isHovered ? 1.0 : 0.0,
|
||||
duration: UIConstants.animationDuration,
|
||||
child: Material(
|
||||
color: AppColors.destructive.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
controller.deleteSection(widget.sectionIndex),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(6),
|
||||
child: Icon(
|
||||
Icons.delete_outline,
|
||||
color: AppColors.destructive,
|
||||
size: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing4),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
onPressed: () => controller.deleteSection(sectionIndex),
|
||||
|
||||
const Divider(height: 1),
|
||||
|
||||
// --- Drop zone indicator ---
|
||||
if (_isDragOver)
|
||||
Container(
|
||||
height: 48,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing12,
|
||||
vertical: UIConstants.spacing8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.accentMuted,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: AppColors.accent.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.add_circle_outline,
|
||||
color: AppColors.accent,
|
||||
size: 15,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Drop exercise here',
|
||||
style: GoogleFonts.inter(
|
||||
color: AppColors.accent,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// --- Exercise list ---
|
||||
if (widget.section.exercises.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing12,
|
||||
vertical: UIConstants.spacing8,
|
||||
),
|
||||
child: ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: widget.section.exercises.length,
|
||||
onReorder: (oldIndex, newIndex) =>
|
||||
controller.reorderExercise(
|
||||
widget.sectionIndex,
|
||||
oldIndex,
|
||||
newIndex,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final exercise = widget.section.exercises[index];
|
||||
return PlanExerciseTile(
|
||||
key: ValueKey(exercise.instanceId),
|
||||
exercise: exercise,
|
||||
sectionIndex: widget.sectionIndex,
|
||||
exerciseIndex: index,
|
||||
plan: widget.plan,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// --- Bottom actions ---
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
UIConstants.spacing12,
|
||||
UIConstants.spacing4,
|
||||
UIConstants.spacing12,
|
||||
UIConstants.spacing12,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_ActionButton(
|
||||
icon: Icons.add,
|
||||
label: 'Add Exercise',
|
||||
onTap: () =>
|
||||
_showExercisePicker(context, (exercise) {
|
||||
controller.addExerciseToSection(
|
||||
widget.sectionIndex,
|
||||
exercise,
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing8),
|
||||
_ActionButton(
|
||||
icon: Icons.create_outlined,
|
||||
label: 'Create New',
|
||||
onTap: () =>
|
||||
_showCreateExerciseDialog(context, controller),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: section.exercises.length,
|
||||
onReorder: (oldIndex, newIndex) =>
|
||||
controller.reorderExercise(sectionIndex, oldIndex, newIndex),
|
||||
itemBuilder: (context, index) {
|
||||
final exercise = section.exercises[index];
|
||||
return PlanExerciseTile(
|
||||
key: ValueKey(exercise.instanceId),
|
||||
exercise: exercise,
|
||||
sectionIndex: sectionIndex,
|
||||
exerciseIndex: index,
|
||||
plan: plan,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: TextButton.icon(
|
||||
onPressed: () {
|
||||
_showExercisePicker(context, (exercise) {
|
||||
controller.addExerciseToSection(sectionIndex, exercise);
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add Exercise'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,22 +289,77 @@ class PlanSectionCard extends ConsumerWidget {
|
||||
builder: (context, scrollController) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
'Select Exercise',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(24, 20, 24, 12),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: AppColors.border)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Select Exercise',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${widget.availableExercises.length} available',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount: availableExercises.length,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UIConstants.spacing8,
|
||||
),
|
||||
itemCount: widget.availableExercises.length,
|
||||
itemBuilder: (context, index) {
|
||||
final exercise = availableExercises[index];
|
||||
final exercise = widget.availableExercises[index];
|
||||
return ListTile(
|
||||
title: Text(exercise.name),
|
||||
subtitle: Text(exercise.muscleGroup ?? ''),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing24,
|
||||
vertical: 4,
|
||||
),
|
||||
leading: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.zinc800,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.fitness_center,
|
||||
size: 16,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
exercise.name,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
subtitle: exercise.muscleGroup != null &&
|
||||
exercise.muscleGroup!.isNotEmpty
|
||||
? Text(
|
||||
exercise.muscleGroup!,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
onSelect(exercise);
|
||||
Navigator.pop(context);
|
||||
@@ -137,4 +374,136 @@ class PlanSectionCard extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showCreateExerciseDialog(
|
||||
BuildContext context,
|
||||
PlanEditorController controller,
|
||||
) {
|
||||
final nameCtrl = TextEditingController();
|
||||
final instructionsCtrl = TextEditingController();
|
||||
final tagsCtrl = TextEditingController();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Create New Exercise'),
|
||||
content: SizedBox(
|
||||
width: UIConstants.dialogWidth,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: nameCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Name *'),
|
||||
autofocus: true,
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing12),
|
||||
TextField(
|
||||
controller: instructionsCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Instructions'),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing12),
|
||||
TextField(
|
||||
controller: tagsCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Tags (comma separated)',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
if (nameCtrl.text.isEmpty) return;
|
||||
Navigator.pop(dialogContext);
|
||||
final exercise = await controller.createExercise(
|
||||
name: nameCtrl.text,
|
||||
instructions: instructionsCtrl.text.isEmpty
|
||||
? null
|
||||
: instructionsCtrl.text,
|
||||
tags: tagsCtrl.text.isEmpty ? null : tagsCtrl.text,
|
||||
);
|
||||
controller.addExerciseToSection(widget.sectionIndex, exercise);
|
||||
},
|
||||
child: const Text('Create & Add'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Small action button (Add Exercise / Create New)
|
||||
// ---------------------------------------------------------------------------
|
||||
class _ActionButton extends StatefulWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ActionButton({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_ActionButton> createState() => _ActionButtonState();
|
||||
}
|
||||
|
||||
class _ActionButtonState extends State<_ActionButton> {
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
onExit: (_) => setState(() => _isHovered = false),
|
||||
child: GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: UIConstants.animationDuration,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: _isHovered ? AppColors.zinc800 : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: _isHovered ? AppColors.zinc600 : AppColors.border,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
widget.icon,
|
||||
size: 13,
|
||||
color: _isHovered
|
||||
? AppColors.textSecondary
|
||||
: AppColors.textMuted,
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
widget.label,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _isHovered
|
||||
? AppColors.textSecondary
|
||||
: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user