Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion lib/core/widgets/app_toast.dart
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
// TODO: Implement lib/core/widgets/app_toast.dart
import 'package:flutter/material.dart';

class AppToast {
const AppToast._();

static ScaffoldFeatureController<SnackBar, SnackBarClosedReason> 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)),
],
),
),
);
}
}
20 changes: 19 additions & 1 deletion lib/features/camera/controller/camera_controller_provider.dart
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
// TODO: Implement lib/features/camera/controller/camera_controller_provider.dart
import 'package:camera/camera.dart';

class CameraControllerProvider {
const CameraControllerProvider();

Future<List<CameraDescription>> available() => availableCameras();

CameraController create({
required CameraDescription camera,
required ResolutionPreset resolutionPreset,
bool enableAudio = false,
}) {
return CameraController(
camera,
resolutionPreset,
enableAudio: enableAudio,
);
}
}
45 changes: 44 additions & 1 deletion lib/features/camera/widgets/camera_controls_bar.dart
Original file line number Diff line number Diff line change
@@ -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',
),
],
),
),
);
}
}
81 changes: 80 additions & 1 deletion lib/features/camera/widgets/camera_top_toolbar.dart
Original file line number Diff line number Diff line change
@@ -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<FlashMode> 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,
),
);
}
53 changes: 52 additions & 1 deletion lib/features/camera/widgets/capture_button.dart
Original file line number Diff line number Diff line change
@@ -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,
Comment on lines +18 to +22
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,
),
),
),
),
);
}
}
44 changes: 43 additions & 1 deletion lib/features/camera/widgets/zoom_selector.dart
Original file line number Diff line number Diff line change
@@ -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<double> onChanged;
final List<double> 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(),
),
),
);
}
}
42 changes: 41 additions & 1 deletion lib/features/detail/widgets/detail_bottom_bar.dart
Original file line number Diff line number Diff line change
@@ -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',
),
Comment on lines +31 to +35
],
),
),
);
}
}
20 changes: 19 additions & 1 deletion lib/features/detail/widgets/detail_location_card.dart
Original file line number Diff line number Diff line change
@@ -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,
);
}
}
45 changes: 44 additions & 1 deletion lib/features/gallery/controller/gallery_controller.dart
Comment thread
krishna3554 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -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<String> _selectedIds = <String>{};
List<AppPhoto> _photos = const [];
bool _loading = false;

List<AppPhoto> get photos => List.unmodifiable(_photos);
Set<String> get selectedIds => Set.unmodifiable(_selectedIds);
bool get isLoading => _loading;
bool get isSelectionMode => _selectedIds.isNotEmpty;

Future<void> load() async {
_loading = true;
notifyListeners();
_photos = await _photoStore.loadPhotos();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset loading when photo load fails

When _photoStore.loadPhotos() throws (for example from corrupt saved metadata or a storage/shared-preferences failure), load() exits before clearing _loading or notifying listeners again. Any UI bound to isLoading will stay stuck in the loading state, and callers cannot repair it because _loading is private; wrap the await in a try/finally or otherwise notify after failures.

Useful? React with 👍 / 👎.

_selectedIds.clear();
_loading = false;
notifyListeners();
}
Comment on lines +20 to +27

void toggleSelection(AppPhoto photo) {
if (!_selectedIds.add(photo.id)) {
_selectedIds.remove(photo.id);
}
notifyListeners();
}

void clearSelection() {
if (_selectedIds.isEmpty) return;
_selectedIds.clear();
notifyListeners();
}

List<AppPhoto> selectedPhotos() =>
_photos.where((photo) => _selectedIds.contains(photo.id)).toList(growable: false);
}
Loading
Loading