From 5ba83b7a32999ec6536cf6615fd2335914e6313e Mon Sep 17 00:00:00 2001 From: Krishna lokhande <87197325+krishna3554@users.noreply.github.com> Date: Fri, 7 Aug 2026 23:31:17 +0530 Subject: [PATCH] Implement reusable feature widgets --- lib/core/widgets/app_toast.dart | 31 ++++++- .../camera_controller_provider.dart | 20 ++++- .../camera/widgets/camera_controls_bar.dart | 45 ++++++++++- .../camera/widgets/camera_top_toolbar.dart | 81 ++++++++++++++++++- .../camera/widgets/capture_button.dart | 53 +++++++++++- .../camera/widgets/zoom_selector.dart | 44 +++++++++- .../detail/widgets/detail_bottom_bar.dart | 42 +++++++++- .../detail/widgets/detail_location_card.dart | 20 ++++- .../controller/gallery_controller.dart | 45 ++++++++++- lib/features/gallery/widgets/media_grid.dart | 44 +++++++++- .../gallery/widgets/video_duration_badge.dart | 38 ++++++++- 11 files changed, 452 insertions(+), 11 deletions(-) diff --git a/lib/core/widgets/app_toast.dart b/lib/core/widgets/app_toast.dart index 674e8e1..c50ea5f 100644 --- a/lib/core/widgets/app_toast.dart +++ b/lib/core/widgets/app_toast.dart @@ -1 +1,30 @@ -// TODO: Implement lib/core/widgets/app_toast.dart +import 'package:flutter/material.dart'; + +class AppToast { + const AppToast._(); + + static ScaffoldFeatureController show( + BuildContext context, { + required String message, + IconData icon = Icons.info_outline, + Duration duration = const Duration(seconds: 3), + }) { + final theme = Theme.of(context); + final messenger = ScaffoldMessenger.of(context)..hideCurrentSnackBar(); + + return messenger.showSnackBar( + SnackBar( + duration: duration, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + content: Row( + children: [ + Icon(icon, color: theme.colorScheme.onInverseSurface), + const SizedBox(width: 12), + Expanded(child: Text(message)), + ], + ), + ), + ); + } +} diff --git a/lib/features/camera/controller/camera_controller_provider.dart b/lib/features/camera/controller/camera_controller_provider.dart index cbdae23..40b6f63 100644 --- a/lib/features/camera/controller/camera_controller_provider.dart +++ b/lib/features/camera/controller/camera_controller_provider.dart @@ -1 +1,19 @@ -// TODO: Implement lib/features/camera/controller/camera_controller_provider.dart +import 'package:camera/camera.dart'; + +class CameraControllerProvider { + const CameraControllerProvider(); + + Future> available() => availableCameras(); + + CameraController create({ + required CameraDescription camera, + required ResolutionPreset resolutionPreset, + bool enableAudio = false, + }) { + return CameraController( + camera, + resolutionPreset, + enableAudio: enableAudio, + ); + } +} diff --git a/lib/features/camera/widgets/camera_controls_bar.dart b/lib/features/camera/widgets/camera_controls_bar.dart index 2ad0350..bc48338 100644 --- a/lib/features/camera/widgets/camera_controls_bar.dart +++ b/lib/features/camera/widgets/camera_controls_bar.dart @@ -1 +1,44 @@ -// TODO: Implement lib/features/camera/widgets/camera_controls_bar.dart +import 'package:flutter/material.dart'; + +import 'capture_button.dart'; + +class CameraControlsBar extends StatelessWidget { + const CameraControlsBar({ + required this.onCapture, + required this.onSwitchCamera, + this.onOpenGallery, + this.isCapturing = false, + super.key, + }); + + final VoidCallback? onCapture; + final VoidCallback? onSwitchCamera; + final VoidCallback? onOpenGallery; + final bool isCapturing; + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 16, 24, 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton.filledTonal( + onPressed: onOpenGallery, + icon: const Icon(Icons.photo_library_outlined), + tooltip: 'Gallery', + ), + CaptureButton(onPressed: onCapture, isBusy: isCapturing), + IconButton.filledTonal( + onPressed: onSwitchCamera, + icon: const Icon(Icons.cameraswitch_outlined), + tooltip: 'Switch camera', + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/camera/widgets/camera_top_toolbar.dart b/lib/features/camera/widgets/camera_top_toolbar.dart index 48f8c8c..c243a8f 100644 --- a/lib/features/camera/widgets/camera_top_toolbar.dart +++ b/lib/features/camera/widgets/camera_top_toolbar.dart @@ -1 +1,80 @@ -// TODO: Implement lib/features/camera/widgets/camera_top_toolbar.dart +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; + +class CameraTopToolbar extends StatelessWidget { + const CameraTopToolbar({ + required this.flashMode, + required this.onFlashModeChanged, + required this.onOpenSettings, + this.onOpenGallery, + super.key, + }); + + final FlashMode flashMode; + final ValueChanged onFlashModeChanged; + final VoidCallback onOpenSettings; + final VoidCallback? onOpenGallery; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + _ToolbarButton( + icon: Icons.photo_library_outlined, + tooltip: 'Open gallery', + onPressed: onOpenGallery, + ), + const Spacer(), + _ToolbarButton( + icon: _flashIcon(flashMode), + tooltip: 'Change flash mode', + onPressed: () => onFlashModeChanged(_nextFlashMode(flashMode)), + ), + const SizedBox(width: 8), + _ToolbarButton( + icon: Icons.settings_outlined, + tooltip: 'Open settings', + onPressed: onOpenSettings, + ), + ], + ), + ), + ); + } + + static FlashMode _nextFlashMode(FlashMode mode) => switch (mode) { + FlashMode.off => FlashMode.auto, + FlashMode.auto => FlashMode.always, + FlashMode.always => FlashMode.torch, + FlashMode.torch => FlashMode.off, + }; + + static IconData _flashIcon(FlashMode mode) => switch (mode) { + FlashMode.off => Icons.flash_off, + FlashMode.auto => Icons.flash_auto, + FlashMode.always => Icons.flash_on, + FlashMode.torch => Icons.highlight, + }; +} + +class _ToolbarButton extends StatelessWidget { + const _ToolbarButton({required this.icon, required this.tooltip, this.onPressed}); + + final IconData icon; + final String tooltip; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) => IconButton.filledTonal( + onPressed: onPressed, + icon: Icon(icon), + tooltip: tooltip, + style: IconButton.styleFrom( + backgroundColor: Colors.black.withValues(alpha: 0.45), + foregroundColor: Colors.white, + ), + ); +} diff --git a/lib/features/camera/widgets/capture_button.dart b/lib/features/camera/widgets/capture_button.dart index 13488b1..e7a75d1 100644 --- a/lib/features/camera/widgets/capture_button.dart +++ b/lib/features/camera/widgets/capture_button.dart @@ -1 +1,52 @@ -// TODO: Implement lib/features/camera/widgets/capture_button.dart +import 'package:flutter/material.dart'; + +class CaptureButton extends StatelessWidget { + const CaptureButton({ + required this.onPressed, + this.isRecording = false, + this.isBusy = false, + super.key, + }); + + final VoidCallback? onPressed; + final bool isRecording; + final bool isBusy; + + @override + Widget build(BuildContext context) { + final enabled = onPressed != null && !isBusy; + return Semantics( + button: true, + label: isRecording ? 'Stop recording' : 'Capture photo', + child: GestureDetector( + onTap: enabled ? onPressed : null, + child: AnimatedOpacity( + opacity: enabled ? 1 : 0.5, + duration: const Duration(milliseconds: 150), + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + width: 78, + height: 78, + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 4), + ), + child: DecoratedBox( + decoration: BoxDecoration( + color: isRecording ? Colors.redAccent : Colors.white, + shape: BoxShape.circle, + ), + child: isBusy + ? const Padding( + padding: EdgeInsets.all(18), + child: CircularProgressIndicator(strokeWidth: 3), + ) + : null, + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/camera/widgets/zoom_selector.dart b/lib/features/camera/widgets/zoom_selector.dart index 7720210..8878658 100644 --- a/lib/features/camera/widgets/zoom_selector.dart +++ b/lib/features/camera/widgets/zoom_selector.dart @@ -1 +1,43 @@ -// TODO: Implement lib/features/camera/widgets/zoom_selector.dart +import 'package:flutter/material.dart'; + +class ZoomSelector extends StatelessWidget { + const ZoomSelector({ + required this.value, + required this.onChanged, + this.options = const [1, 2, 3], + super.key, + }); + + final double value; + final ValueChanged onChanged; + final List options; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.45), + borderRadius: BorderRadius.circular(999), + ), + child: Padding( + padding: const EdgeInsets.all(4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: options.map((zoom) { + final selected = (value - zoom).abs() < 0.15; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: ChoiceChip( + label: Text('${zoom.toStringAsFixed(zoom % 1 == 0 ? 0 : 1)}x'), + selected: selected, + onSelected: (_) => onChanged(zoom), + showCheckmark: false, + visualDensity: VisualDensity.compact, + ), + ); + }).toList(), + ), + ), + ); + } +} diff --git a/lib/features/detail/widgets/detail_bottom_bar.dart b/lib/features/detail/widgets/detail_bottom_bar.dart index 69e2cab..ec9a905 100644 --- a/lib/features/detail/widgets/detail_bottom_bar.dart +++ b/lib/features/detail/widgets/detail_bottom_bar.dart @@ -1 +1,41 @@ -// TODO: Implement lib/features/detail/widgets/detail_bottom_bar.dart +import 'package:flutter/material.dart'; + +class DetailBottomBar extends StatelessWidget { + const DetailBottomBar({ + required this.onShowQr, + required this.onShare, + required this.onDelete, + super.key, + }); + + final VoidCallback? onShowQr; + final VoidCallback? onShare; + final VoidCallback? onDelete; + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: Container( + color: Theme.of(context).colorScheme.surface, + height: 64, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + onPressed: onShowQr, + icon: const Icon(Icons.qr_code_2), + tooltip: 'Show location QR', + ), + IconButton(onPressed: onShare, icon: const Icon(Icons.share), tooltip: 'Share media'), + IconButton( + onPressed: onDelete, + icon: const Icon(Icons.delete_outline, color: Colors.red), + tooltip: 'Delete media', + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/detail/widgets/detail_location_card.dart b/lib/features/detail/widgets/detail_location_card.dart index 39c1262..597ad9d 100644 --- a/lib/features/detail/widgets/detail_location_card.dart +++ b/lib/features/detail/widgets/detail_location_card.dart @@ -1 +1,19 @@ -// TODO: Implement lib/features/detail/widgets/detail_location_card.dart +import 'package:flutter/material.dart'; + +import '../../../core/widgets/location_stamp_card.dart'; +import '../../../models/location_info.dart'; + +class DetailLocationCard extends StatelessWidget { + const DetailLocationCard({required this.locationInfo, this.width, super.key}); + + final LocationInfo? locationInfo; + final double? width; + + @override + Widget build(BuildContext context) { + return LocationStampCard( + locationInfo: locationInfo, + cardWidth: width ?? MediaQuery.sizeOf(context).width * 0.85, + ); + } +} diff --git a/lib/features/gallery/controller/gallery_controller.dart b/lib/features/gallery/controller/gallery_controller.dart index bf19a81..c0e48e6 100644 --- a/lib/features/gallery/controller/gallery_controller.dart +++ b/lib/features/gallery/controller/gallery_controller.dart @@ -1 +1,44 @@ -// TODO: Implement lib/features/gallery/controller/gallery_controller.dart +import 'package:flutter/foundation.dart'; + +import '../../../models/app_photo.dart'; +import '../../../services/app_photo_store.dart'; + +class GalleryController extends ChangeNotifier { + GalleryController({AppPhotoStore photoStore = const AppPhotoStore()}) + : _photoStore = photoStore; + + final AppPhotoStore _photoStore; + final Set _selectedIds = {}; + List _photos = const []; + bool _loading = false; + + List get photos => List.unmodifiable(_photos); + Set get selectedIds => Set.unmodifiable(_selectedIds); + bool get isLoading => _loading; + bool get isSelectionMode => _selectedIds.isNotEmpty; + + Future load() async { + _loading = true; + notifyListeners(); + _photos = await _photoStore.loadPhotos(); + _selectedIds.clear(); + _loading = false; + notifyListeners(); + } + + void toggleSelection(AppPhoto photo) { + if (!_selectedIds.add(photo.id)) { + _selectedIds.remove(photo.id); + } + notifyListeners(); + } + + void clearSelection() { + if (_selectedIds.isEmpty) return; + _selectedIds.clear(); + notifyListeners(); + } + + List selectedPhotos() => + _photos.where((photo) => _selectedIds.contains(photo.id)).toList(growable: false); +} diff --git a/lib/features/gallery/widgets/media_grid.dart b/lib/features/gallery/widgets/media_grid.dart index 7e611df..f2e2db2 100644 --- a/lib/features/gallery/widgets/media_grid.dart +++ b/lib/features/gallery/widgets/media_grid.dart @@ -1 +1,43 @@ -// TODO: Implement lib/features/gallery/widgets/media_grid.dart +import 'package:flutter/material.dart'; +import 'package:photo_manager/photo_manager.dart'; + +import 'media_tile.dart'; + +class MediaGrid extends StatelessWidget { + const MediaGrid({ + required this.assets, + required this.selectedIds, + required this.onAssetTap, + this.crossAxisCount = 3, + super.key, + }); + + final List assets; + final Set selectedIds; + final ValueChanged onAssetTap; + final int crossAxisCount; + + @override + Widget build(BuildContext context) { + final selectMode = selectedIds.isNotEmpty; + return GridView.builder( + padding: const EdgeInsets.all(2), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: 2, + mainAxisSpacing: 2, + ), + itemCount: assets.length, + itemBuilder: (context, index) { + final asset = assets[index]; + return MediaTile( + asset: asset, + isSelected: selectedIds.contains(asset.id), + selectMode: selectMode, + onTap: () => onAssetTap(asset), + index: index, + ); + }, + ); + } +} diff --git a/lib/features/gallery/widgets/video_duration_badge.dart b/lib/features/gallery/widgets/video_duration_badge.dart index 5083b94..90cc4b4 100644 --- a/lib/features/gallery/widgets/video_duration_badge.dart +++ b/lib/features/gallery/widgets/video_duration_badge.dart @@ -1 +1,37 @@ -// TODO: Implement lib/features/gallery/widgets/video_duration_badge.dart +import 'package:flutter/material.dart'; + +import '../../../core/utils/date_utils.dart'; + +class VideoDurationBadge extends StatelessWidget { + const VideoDurationBadge({required this.duration, super.key}); + + final Duration duration; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.72), + borderRadius: BorderRadius.circular(999), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.play_arrow_rounded, color: Colors.white, size: 14), + const SizedBox(width: 2), + Text( + AppDateUtils.formatDuration(duration), + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ); + } +}