diff --git a/lib/cubit/canvas_cubit.dart b/lib/cubit/canvas_cubit.dart index 6e12edbd..aaa0ab6f 100644 --- a/lib/cubit/canvas_cubit.dart +++ b/lib/cubit/canvas_cubit.dart @@ -13,6 +13,7 @@ import 'package:path/path.dart' as path; import 'package:texterra/utils/custom_snackbar.dart'; import '../models/text_item_model.dart'; import '../models/draw_model.dart'; +import '../models/history_entry.dart'; import 'canvas_state.dart'; class CanvasCubit extends Cubit { @@ -20,6 +21,27 @@ class CanvasCubit extends Cubit { CanvasCubit() : super(CanvasState.initial()); + // Helper method to add a new state to history + void _addToHistory(CanvasState newState, {String? actionDescription}) { + final newEntry = HistoryEntry( + state: newState, + timestamp: DateTime.now(), + actionDescription: actionDescription, + ); + + // If we're not at the end of history, truncate history after current index + final baseHistory = state.currentHistoryIndex < state.history.length - 1 + ? state.history.sublist(0, state.currentHistoryIndex + 1) + : state.history; + + final newHistory = [...baseHistory, newEntry]; + + emit(newState.copyWith( + history: newHistory, + currentHistoryIndex: newHistory.length - 1, + )); + } + //method to toggle the color tray void toggleTray() { emit(state.copyWith(isTrayShown: !state.isTrayShown)); @@ -40,7 +62,7 @@ class CanvasCubit extends Cubit { updatedItems[index] = updatedItems[index].copyWith( hasShadow: !updatedItems[index].hasShadow, ); - _updateState(textItems: updatedItems); + _updateState(textItems: updatedItems, actionDescription: 'Toggled text shadow'); } // Change shadow color @@ -52,7 +74,7 @@ class CanvasCubit extends Cubit { hasShadow: true, // Automatically enable shadow when changing color shadowColor: color, ); - _updateState(textItems: updatedItems); + _updateState(textItems: updatedItems, actionDescription: 'Changed shadow color'); } // Change shadow blur radius @@ -185,14 +207,11 @@ class CanvasCubit extends Cubit { color: ColorConstants.uiWhite, // My Default color for the text ); final updatedItems = List.from(state.textItems)..add(newTextItem); - emit( - state.copyWith( - textItems: updatedItems, - selectedTextItemIndex: updatedItems.length - 1, - history: [...state.history, state], - future: [], - ), + final newState = state.copyWith( + textItems: updatedItems, + selectedTextItemIndex: updatedItems.length - 1, ); + _addToHistory(newState, actionDescription: 'Added text: "${text.length > 20 ? '${text.substring(0, 20)}...' : text}"'); } // Method to toggle text highlighting @@ -246,7 +265,7 @@ class CanvasCubit extends Cubit { // method to change background color void changeBackgroundColor(Color color) { - _updateState(backgroundColor: color); + _updateState(backgroundColor: color, actionDescription: 'Changed background color'); } // Method to upload background image from gallery @@ -437,24 +456,24 @@ class CanvasCubit extends Cubit { // method to undo changes and emit it void undo() { - if (state.history.isNotEmpty) { - final previousState = state.history.last; - final newHistory = List.from(state.history)..removeLast(); - emit(previousState.copyWith( - history: newHistory, - future: [state, ...state.future], + if (state.currentHistoryIndex > 0) { + final newIndex = state.currentHistoryIndex - 1; + final targetState = state.history[newIndex].state; + emit(targetState.copyWith( + history: state.history, + currentHistoryIndex: newIndex, )); } } // method to redo changes and emit it void redo() { - if (state.future.isNotEmpty) { - final nextState = state.future.first; - final newFuture = List.from(state.future)..removeAt(0); - emit(nextState.copyWith( - future: newFuture, - history: [...state.history, state], + if (state.currentHistoryIndex < state.history.length - 1) { + final newIndex = state.currentHistoryIndex + 1; + final targetState = state.history[newIndex].state; + emit(targetState.copyWith( + history: state.history, + currentHistoryIndex: newIndex, )); } } @@ -466,19 +485,16 @@ class CanvasCubit extends Cubit { _deleteImageFile(state.backgroundImagePath!); } - emit( - state.copyWith( - textItems: [], - drawPaths: [], - history: [...state.history, state], - future: [], - selectedTextItemIndex: null, - deselect: true, - isDrawingMode: false, // Exit drawing mode when clearing - clearCurrentPageName: true, - clearBackgroundImage: true, - ), + final newState = state.copyWith( + textItems: [], + drawPaths: [], + selectedTextItemIndex: null, + deselect: true, + isDrawingMode: false, // Exit drawing mode when clearing + clearCurrentPageName: true, + clearBackgroundImage: true, ); + _addToHistory(newState, actionDescription: 'Cleared canvas'); CustomSnackbar.showInfo('Canvas cleared'); } @@ -487,26 +503,26 @@ class CanvasCubit extends Cubit { Color? backgroundColor, String? backgroundImagePath, bool clearBackgroundImage = false, + String? actionDescription, }) { final newState = state.copyWith( textItems: textItems ?? state.textItems, backgroundColor: backgroundColor, backgroundImagePath: backgroundImagePath, clearBackgroundImage: clearBackgroundImage, - history: [...state.history, state], - future: [], ); - emit(newState); + _addToHistory(newState, actionDescription: actionDescription); } void deleteText(int index) { + final deletedItem = state.textItems[index]; final updatedList = List.from(state.textItems)..removeAt(index); - emit(state.copyWith( - textItems: updatedList, - selectedTextItemIndex: null, - history: [...state.history, state], - future: [], - deselect: true)); + final newState = state.copyWith( + textItems: updatedList, + selectedTextItemIndex: null, + deselect: true, + ); + _addToHistory(newState, actionDescription: 'Deleted text: "${deletedItem.text.length > 20 ? '${deletedItem.text.substring(0, 20)}...' : deletedItem.text}"'); } Future savePage(String pageName, {String? label, int? color}) async { @@ -955,16 +971,8 @@ class CanvasCubit extends Cubit { ); final newPaths = List.from(state.drawPaths)..add(newPath); - - // Save current state to history - final historyState = state.copyWith(); - final newHistory = List.from(state.history)..add(historyState); - - emit(state.copyWith( - drawPaths: newPaths, - history: newHistory, - future: [], // Clear future as we've made a new action - )); + final newState = state.copyWith(drawPaths: newPaths); + _addToHistory(newState, actionDescription: 'Started drawing'); } // Update the current drawing path with a new point @@ -1001,15 +1009,8 @@ class CanvasCubit extends Cubit { void clearDrawings() { if (state.drawPaths.isEmpty) return; - // Save current state to history - final historyState = state.copyWith(); - final newHistory = List.from(state.history)..add(historyState); - - emit(state.copyWith( - drawPaths: [], - history: newHistory, - future: [], // Clear future as we've made a new action - )); + final newState = state.copyWith(drawPaths: []); + _addToHistory(newState, actionDescription: 'Cleared all drawings'); CustomSnackbar.showInfo('Drawings cleared'); } @@ -1017,19 +1018,12 @@ class CanvasCubit extends Cubit { void undoLastDrawing() { if (state.drawPaths.isEmpty) return; - // Save current state to history - final historyState = state.copyWith(); - final newHistory = List.from(state.history)..add(historyState); - // Remove the last path final newPaths = List.from(state.drawPaths); newPaths.removeLast(); - emit(state.copyWith( - drawPaths: newPaths, - history: newHistory, - future: [], // Clear future as we've made a new action - )); + final newState = state.copyWith(drawPaths: newPaths); + _addToHistory(newState, actionDescription: 'Undid last drawing stroke'); CustomSnackbar.showInfo('Last stroke undone'); } diff --git a/lib/cubit/canvas_state.dart b/lib/cubit/canvas_state.dart index 2f581520..eed1d625 100644 --- a/lib/cubit/canvas_state.dart +++ b/lib/cubit/canvas_state.dart @@ -2,12 +2,13 @@ import 'package:flutter/material.dart'; import '../models/text_item_model.dart'; import '../constants/color_constants.dart'; import '../models/draw_model.dart'; +import '../models/history_entry.dart'; class CanvasState { final List textItems; final List drawPaths; - final List history; - final List future; + final List history; + final int currentHistoryIndex; final Color backgroundColor; final String? backgroundImagePath; final int? selectedTextItemIndex; @@ -22,7 +23,7 @@ class CanvasState { required this.textItems, required this.drawPaths, required this.history, - required this.future, + required this.currentHistoryIndex, this.backgroundColor = ColorConstants.backgroundDarkGray, this.backgroundImagePath, this.selectedTextItemIndex, @@ -35,11 +36,11 @@ class CanvasState { }); factory CanvasState.initial() { - return const CanvasState( + final initialState = const CanvasState( textItems: [], drawPaths: [], history: [], - future: [], + currentHistoryIndex: 0, backgroundColor: ColorConstants.backgroundDarkGray, backgroundImagePath: null, selectedTextItemIndex: null, @@ -50,13 +51,25 @@ class CanvasState { currentStrokeWidth: 5.0, currentBrushType: BrushType.brush, ); + + // Add initial state to history + final initialEntry = HistoryEntry( + state: initialState, + timestamp: DateTime.now(), + actionDescription: 'Initial state', + ); + + return initialState.copyWith( + history: [initialEntry], + currentHistoryIndex: 0, + ); } CanvasState copyWith({ List? textItems, List? drawPaths, - List? history, - List? future, + List? history, + int? currentHistoryIndex, Color? backgroundColor, String? backgroundImagePath, bool clearBackgroundImage = false, @@ -74,7 +87,7 @@ class CanvasState { textItems: textItems ?? this.textItems, drawPaths: drawPaths ?? this.drawPaths, history: history ?? this.history, - future: future ?? this.future, + currentHistoryIndex: currentHistoryIndex ?? this.currentHistoryIndex, backgroundColor: backgroundColor ?? this.backgroundColor, backgroundImagePath: clearBackgroundImage ? null @@ -104,8 +117,6 @@ class CanvasState { backgroundImagePath == other.backgroundImagePath && selectedTextItemIndex == other.selectedTextItemIndex && isDrawingMode == other.isDrawingMode && - history == other.history && - future == other.future && isTrayShown == other.isTrayShown && currentPageName == other.currentPageName && currentDrawColor == other.currentDrawColor && @@ -119,8 +130,6 @@ class CanvasState { backgroundColor.hashCode ^ backgroundImagePath.hashCode ^ selectedTextItemIndex.hashCode ^ - history.hashCode ^ - future.hashCode ^ isTrayShown.hashCode ^ isDrawingMode.hashCode ^ currentDrawColor.hashCode ^ diff --git a/lib/models/history_entry.dart b/lib/models/history_entry.dart new file mode 100644 index 00000000..8e9d2995 --- /dev/null +++ b/lib/models/history_entry.dart @@ -0,0 +1,31 @@ +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'canvas_state.dart'; + +class HistoryEntry { + final CanvasState state; + final DateTime timestamp; + final String? actionDescription; + ui.Image? thumbnail; + + HistoryEntry({ + required this.state, + required this.timestamp, + this.actionDescription, + this.thumbnail, + }); + + HistoryEntry copyWith({ + CanvasState? state, + DateTime? timestamp, + String? actionDescription, + ui.Image? thumbnail, + }) { + return HistoryEntry( + state: state ?? this.state, + timestamp: timestamp ?? this.timestamp, + actionDescription: actionDescription ?? this.actionDescription, + thumbnail: thumbnail ?? this.thumbnail, + ); + } +} \ No newline at end of file diff --git a/lib/models/history_node.dart b/lib/models/history_node.dart new file mode 100644 index 00000000..171ef6f3 --- /dev/null +++ b/lib/models/history_node.dart @@ -0,0 +1,73 @@ +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'canvas_state.dart'; + +class HistoryNode { + final String id; + final CanvasState state; + final DateTime timestamp; + final ui.Image? thumbnail; + final List children; + final HistoryNode? parent; + final String? actionDescription; + + const HistoryNode({ + required this.id, + required this.state, + required this.timestamp, + this.thumbnail, + this.children = const [], + this.parent, + this.actionDescription, + }); + + HistoryNode copyWith({ + String? id, + CanvasState? state, + DateTime? timestamp, + ui.Image? thumbnail, + List? children, + HistoryNode? parent, + String? actionDescription, + }) { + return HistoryNode( + id: id ?? this.id, + state: state ?? this.state, + timestamp: timestamp ?? this.timestamp, + thumbnail: thumbnail ?? this.thumbnail, + children: children ?? this.children, + parent: parent ?? this.parent, + actionDescription: actionDescription ?? this.actionDescription, + ); + } + + HistoryNode addChild(HistoryNode child) { + return copyWith( + children: [...children, child], + ); + } + + bool get isLeaf => children.isEmpty; + + bool get hasBranches => children.length > 1; + + int get depth { + int d = 0; + HistoryNode? current = parent; + while (current != null) { + d++; + current = current.parent; + } + return d; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is HistoryNode && + runtimeType == other.runtimeType && + id == other.id; + + @override + int get hashCode => id.hashCode; +} \ No newline at end of file diff --git a/lib/ui/screens/canvas_screen.dart b/lib/ui/screens/canvas_screen.dart index 31a90fdf..ced51c0a 100644 --- a/lib/ui/screens/canvas_screen.dart +++ b/lib/ui/screens/canvas_screen.dart @@ -14,6 +14,7 @@ import '../widgets/font_controls.dart'; import '../widgets/background_color_tray.dart'; import '../widgets/background_options_sheet.dart'; import '../widgets/drawing_canvas.dart'; +import '../widgets/history_timeline.dart'; import '../../utils/custom_snackbar.dart'; import '../../utils/web_utils.dart'; @@ -31,6 +32,16 @@ class CanvasScreen extends StatelessWidget { ); } + void _showHistoryTimeline(BuildContext context) { + showDialog( + context: context, + builder: (context) => const Dialog( + backgroundColor: Colors.transparent, + child: HistoryTimeline(), + ), + ); + } + // Handle keyboard shortcut actions void _handleSave(BuildContext context) { final cubit = context.read(); @@ -49,7 +60,7 @@ class CanvasScreen extends StatelessWidget { void _handleUndo(BuildContext context) { final cubit = context.read(); - if (cubit.state.history.isNotEmpty) { + if (cubit.state.currentHistoryIndex > 0) { cubit.undo(); } else { CustomSnackbar.showInfo('Nothing to undo'); @@ -58,7 +69,7 @@ class CanvasScreen extends StatelessWidget { void _handleRedo(BuildContext context) { final cubit = context.read(); - if (cubit.state.future.isNotEmpty) { + if (cubit.state.currentHistoryIndex < cubit.state.history.length - 1) { cubit.redo(); } else { CustomSnackbar.showInfo('Nothing to redo'); @@ -147,6 +158,11 @@ class CanvasScreen extends StatelessWidget { icon: const Icon(Icons.redo, color: ColorConstants.uiIconBlack), onPressed: () => _handleRedo(context), ), + IconButton( + tooltip: "History Timeline", + icon: const Icon(Icons.timeline, color: ColorConstants.uiIconBlack), + onPressed: () => _showHistoryTimeline(context), + ), BlocBuilder( builder: (context, state) { return PopupMenuButton( diff --git a/lib/ui/widgets/canvas_thumbnail.dart b/lib/ui/widgets/canvas_thumbnail.dart new file mode 100644 index 00000000..7aebbde7 --- /dev/null +++ b/lib/ui/widgets/canvas_thumbnail.dart @@ -0,0 +1,168 @@ +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import '../../cubit/canvas_state.dart'; +import '../../models/draw_model.dart'; + +class CanvasThumbnail extends StatefulWidget { + final CanvasState state; + final double width; + final double height; + final Function(ui.Image?)? onThumbnailGenerated; + + const CanvasThumbnail({ + super.key, + required this.state, + this.width = 120, + this.height = 80, + this.onThumbnailGenerated, + }); + + @override + State createState() => _CanvasThumbnailState(); +} + +class _CanvasThumbnailState extends State { + final GlobalKey _repaintBoundaryKey = GlobalKey(); + + @override + void initState() { + super.initState(); + // Generate thumbnail after the widget is built + WidgetsBinding.instance.addPostFrameCallback((_) { + _generateThumbnail(); + }); + } + + @override + void didUpdateWidget(CanvasThumbnail oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.state != widget.state) { + // Regenerate thumbnail when state changes + WidgetsBinding.instance.addPostFrameCallback((_) { + _generateThumbnail(); + }); + } + } + + Future _generateThumbnail() async { + try { + final RenderRepaintBoundary boundary = _repaintBoundaryKey.currentContext! + .findRenderObject() as RenderRepaintBoundary; + final ui.Image image = await boundary.toImage(pixelRatio: 0.5); + widget.onThumbnailGenerated?.call(image); + } catch (e) { + // If thumbnail generation fails, call with null + widget.onThumbnailGenerated?.call(null); + } + } + + @override + Widget build(BuildContext context) { + return RepaintBoundary( + key: _repaintBoundaryKey, + child: Container( + width: widget.width, + height: widget.height, + decoration: BoxDecoration( + color: widget.state.backgroundColor, + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.white.withOpacity(0.2)), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Stack( + children: [ + // Background image if exists + if (widget.state.backgroundImagePath != null) + Positioned.fill( + child: Image.asset( + widget.state.backgroundImagePath!, + fit: BoxFit.cover, + ), + ), + + // Drawing paths + if (widget.state.drawPaths.isNotEmpty) + Positioned.fill( + child: CustomPaint( + painter: DrawingCanvasPainter( + drawPaths: widget.state.drawPaths, + isThumbnail: true, + ), + ), + ), + + // Text items (simplified for thumbnail) + ...widget.state.textItems.map((textItem) { + final scaleX = widget.width / (MediaQuery.of(context).size.width); + final scaleY = widget.height / (MediaQuery.of(context).size.height); + return Positioned( + left: textItem.x * scaleX, + top: textItem.y * scaleY, + child: Container( + constraints: BoxConstraints( + maxWidth: widget.width * 0.8, + maxHeight: widget.height * 0.8, + ), + child: Text( + textItem.text, + style: TextStyle( + color: textItem.color, + fontSize: (textItem.fontSize * 0.3).clamp(8.0, 20.0), + fontWeight: textItem.fontWeight, + fontStyle: textItem.fontStyle, + fontFamily: textItem.fontFamily, + decoration: textItem.isUnderlined ? TextDecoration.underline : null, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ); + }), + ], + ), + ), + ), + ); + } +} + +// Simplified painter for thumbnails +class DrawingCanvasPainter extends CustomPainter { + final List drawPaths; + final bool isThumbnail; + + DrawingCanvasPainter({ + required this.drawPaths, + this.isThumbnail = false, + }); + + @override + void paint(Canvas canvas, Size size) { + for (final path in drawPaths) { + final paint = Paint() + ..color = path.color + ..strokeWidth = isThumbnail ? path.strokeWidth * 0.5 : path.strokeWidth + ..strokeCap = path.strokeCap + ..strokeJoin = StrokeJoin.round + ..style = path.isFill ? PaintingStyle.fill : PaintingStyle.stroke; + + final pathObj = Path(); + if (path.points.isNotEmpty) { + pathObj.moveTo(path.points[0].offset.dx, path.points[0].offset.dy); + for (int i = 1; i < path.points.length; i++) { + pathObj.lineTo(path.points[i].offset.dx, path.points[i].offset.dy); + } + } + + canvas.drawPath(pathObj, paint); + } + } + + @override + bool shouldRepaint(DrawingCanvasPainter oldDelegate) { + return oldDelegate.drawPaths != drawPaths; + } +} \ No newline at end of file diff --git a/lib/ui/widgets/history_timeline.dart b/lib/ui/widgets/history_timeline.dart new file mode 100644 index 00000000..420125f8 --- /dev/null +++ b/lib/ui/widgets/history_timeline.dart @@ -0,0 +1,221 @@ +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../cubit/canvas_cubit.dart'; +import '../../cubit/canvas_state.dart'; +import '../../models/history_entry.dart'; +import 'canvas_thumbnail.dart'; + +class HistoryTimeline extends StatefulWidget { + const HistoryTimeline({super.key}); + + @override + State createState() => _HistoryTimelineState(); +} + +class _HistoryTimelineState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final history = state.history; + final currentIndex = state.currentHistoryIndex; + + return Container( + width: 300, + height: 400, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.8), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withOpacity(0.2)), + ), + child: Column( + children: [ + // Header + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + child: Row( + children: [ + const Icon(Icons.history, color: Colors.white), + const SizedBox(width: 8), + const Text( + 'History Timeline', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ), + + // Timeline + Expanded( + child: history.isEmpty + ? const Center(child: Text('No history available', style: TextStyle(color: Colors.white))) + : ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(8), + itemCount: history.length, + itemBuilder: (context, index) { + final entry = history[index]; + final isCurrent = index == currentIndex; + + return _HistoryEntryTile( + entry: entry, + isCurrent: isCurrent, + onTap: () => _jumpToEntry(context, index), + ); + }, + ), + ), + ], + ), + ); + }, + ); + } + + void _jumpToEntry(BuildContext context, int index) { + final cubit = context.read(); + final targetState = cubit.state.history[index].state; + cubit.emit(targetState.copyWith( + history: cubit.state.history, + currentHistoryIndex: index, + )); + } +} + +class _HistoryEntryTile extends StatefulWidget { + final HistoryEntry entry; + final bool isCurrent; + final VoidCallback onTap; + + const _HistoryEntryTile({ + required this.entry, + required this.isCurrent, + required this.onTap, + }); + + @override + State<_HistoryEntryTile> createState() => _HistoryEntryTileState(); +} + +class _HistoryEntryTileState extends State<_HistoryEntryTile> { + ui.Image? _thumbnail; + + @override + Widget build(BuildContext context) { + final timeAgo = _formatTimeAgo(widget.entry.timestamp); + + return GestureDetector( + onTap: widget.onTap, + child: Container( + margin: const EdgeInsets.only(bottom: 4), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: widget.isCurrent + ? Colors.blue.withOpacity(0.3) + : Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: widget.isCurrent ? Colors.blue : Colors.white.withOpacity(0.2), + width: widget.isCurrent ? 2 : 1, + ), + ), + child: Row( + children: [ + // Thumbnail + SizedBox( + width: 40, + height: 30, + child: CanvasThumbnail( + state: widget.entry.state, + width: 40, + height: 30, + onThumbnailGenerated: (image) { + if (mounted) { + setState(() { + _thumbnail = image; + }); + } + }, + ), + ), + + const SizedBox(width: 8), + + // Content + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.entry.actionDescription ?? 'Action', + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: widget.isCurrent ? FontWeight.bold : FontWeight.normal, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + timeAgo, + style: TextStyle( + color: Colors.white.withOpacity(0.7), + fontSize: 10, + ), + ), + ], + ), + ), + + // Current indicator + if (widget.isCurrent) + const Icon( + Icons.radio_button_checked, + color: Colors.blue, + size: 16, + ), + ], + ), + ), + ); + } + + String _formatTimeAgo(DateTime timestamp) { + final now = DateTime.now(); + final difference = now.difference(timestamp); + + if (difference.inSeconds < 60) { + return '${difference.inSeconds}s ago'; + } else if (difference.inMinutes < 60) { + return '${difference.inMinutes}m ago'; + } else if (difference.inHours < 24) { + return '${difference.inHours}h ago'; + } else { + return '${difference.inDays}d ago'; + } + } +} \ No newline at end of file