Improved: Lock to record hands-free

This commit is contained in:
otsmr 2026-04-29 17:12:22 +02:00
parent b2e9b04659
commit 51477e3f51
3 changed files with 145 additions and 3 deletions

View file

@ -8,6 +8,7 @@
- Improved: Show ⌛ instead of the flame icon when it is about to expire - Improved: Show ⌛ instead of the flame icon when it is about to expire
- Improved: FAQ is now in the app rather than opening in the browser - Improved: FAQ is now in the app rather than opening in the browser
- Improved: Videos can now be paused - Improved: Videos can now be paused
- Improved: Lock to record hands-free
- Fix: Many smaller issues - Fix: Many smaller issues
## 0.1.8 ## 0.1.8

View file

@ -9,23 +9,29 @@ class CameraBottomControls extends StatelessWidget {
const CameraBottomControls({ const CameraBottomControls({
required this.mainController, required this.mainController,
required this.isVideoRecording, required this.isVideoRecording,
required this.videoRecordingLocked,
required this.isFront, required this.isFront,
required this.keyTriggerButton, required this.keyTriggerButton,
required this.keyLockButton,
required this.onTakePicture, required this.onTakePicture,
required this.onPressSideButtonLeft, required this.onPressSideButtonLeft,
required this.onPressSideButtonRight, required this.onPressSideButtonRight,
required this.updateScaleFactor, required this.updateScaleFactor,
required this.onStopVideoRecording,
super.key, super.key,
}); });
final MainCameraController mainController; final MainCameraController mainController;
final bool isVideoRecording; final bool isVideoRecording;
final bool videoRecordingLocked;
final bool isFront; final bool isFront;
final GlobalKey keyTriggerButton; final GlobalKey keyTriggerButton;
final GlobalKey keyLockButton;
final VoidCallback onTakePicture; final VoidCallback onTakePicture;
final VoidCallback onPressSideButtonLeft; final VoidCallback onPressSideButtonLeft;
final VoidCallback onPressSideButtonRight; final VoidCallback onPressSideButtonRight;
final Future<void> Function(double) updateScaleFactor; final Future<void> Function(double) updateScaleFactor;
final VoidCallback onStopVideoRecording;
MainCameraController get mc => mainController; MainCameraController get mc => mainController;
@ -57,11 +63,16 @@ class CameraBottomControls extends StatelessWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (!isVideoRecording) _buildSideButtonLeft(), if (!isVideoRecording)
_buildSideButtonLeft()
else
_buildLockOrStopButton(),
_buildShutterButton(), _buildShutterButton(),
if (!isVideoRecording) if (!isVideoRecording)
if (isFront) if (isFront)
_buildSideButtonRight() _buildSideButtonRight()
else
const SizedBox(width: 80)
else else
const SizedBox(width: 80), const SizedBox(width: 80),
], ],
@ -72,6 +83,34 @@ class CameraBottomControls extends StatelessWidget {
); );
} }
Widget _buildLockOrStopButton() {
if (videoRecordingLocked) {
// Show stop button
return GestureDetector(
onTap: onStopVideoRecording,
child: Container(
key: keyLockButton,
height: 50,
width: 80,
padding: const EdgeInsets.all(2),
child: Center(
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(6),
),
),
),
),
);
} else {
// Show animated lock icon (slide here to lock)
return _AnimatedLockButton(keyLockButton: keyLockButton);
}
}
Widget _buildSideButtonLeft() { Widget _buildSideButtonLeft() {
return GestureDetector( return GestureDetector(
onTap: onPressSideButtonLeft, onTap: onPressSideButtonLeft,
@ -144,3 +183,70 @@ class CameraBottomControls extends StatelessWidget {
); );
} }
} }
class _AnimatedLockButton extends StatefulWidget {
const _AnimatedLockButton({required this.keyLockButton});
final GlobalKey keyLockButton;
@override
State<_AnimatedLockButton> createState() => _AnimatedLockButtonState();
}
class _AnimatedLockButtonState extends State<_AnimatedLockButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnim;
late Animation<double> _opacityAnim;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
)..repeat(reverse: true);
_scaleAnim = Tween<double>(begin: 0.85, end: 1.15).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
_opacityAnim = Tween<double>(begin: 0.5, end: 1).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(
opacity: _opacityAnim.value,
child: Transform.scale(
scale: _scaleAnim.value,
child: child,
),
);
},
child: Container(
key: widget.keyLockButton,
height: 50,
width: 80,
padding: const EdgeInsets.all(2),
child: const Center(
child: FaIcon(
FontAwesomeIcons.lock,
color: Colors.white,
size: 25,
),
),
),
);
}
}

View file

@ -115,9 +115,11 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
bool _hasAudioPermission = true; bool _hasAudioPermission = true;
DateTime? _videoRecordingStarted; DateTime? _videoRecordingStarted;
Timer? _videoRecordingTimer; Timer? _videoRecordingTimer;
bool _videoRecordingLocked = false;
DateTime _currentTime = clock.now(); DateTime _currentTime = clock.now();
final GlobalKey keyTriggerButton = GlobalKey(); final GlobalKey keyTriggerButton = GlobalKey();
final GlobalKey keyLockButton = GlobalKey();
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
MainCameraController get mc => widget.mainCameraController; MainCameraController get mc => widget.mainCameraController;
@ -399,6 +401,32 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
return; return;
} }
// Check if the finger moved over the lock button during video recording
if (mc.isVideoRecording && !_videoRecordingLocked) {
final lockContext = keyLockButton.currentContext;
if (lockContext != null) {
final lockRenderBox = lockContext.findRenderObject() as RenderBox?;
if (lockRenderBox != null) {
final lockLocalPosition = lockRenderBox.globalToLocal(
// ignore: avoid_dynamic_calls
details.globalPosition as Offset,
);
final lockRect = Rect.fromLTWH(
0,
0,
lockRenderBox.size.width,
lockRenderBox.size.height,
);
if (lockRect.contains(lockLocalPosition)) {
setState(() {
_videoRecordingLocked = true;
});
await HapticFeedback.heavyImpact();
}
}
}
}
setState(() { setState(() {
mc.selectedCameraDetails.scaleFactor = mc.selectedCameraDetails.scaleFactor =
(_baseScaleFactor + (_baseScaleFactor +
@ -537,7 +565,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
} }
Future<void> stopVideoRecording() async { Future<void> stopVideoRecording({bool force = false}) async {
// If recording is locked, only stop when explicitly forced (e.g. stop button tap)
if (_videoRecordingLocked && !force) return;
if (_videoRecordingTimer != null) { if (_videoRecordingTimer != null) {
_videoRecordingTimer?.cancel(); _videoRecordingTimer?.cancel();
_videoRecordingTimer = null; _videoRecordingTimer = null;
@ -548,6 +579,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
setState(() { setState(() {
_videoRecordingStarted = null; _videoRecordingStarted = null;
mc.isVideoRecording = false; mc.isVideoRecording = false;
_videoRecordingLocked = false;
}); });
if (mc.cameraController == null || if (mc.cameraController == null ||
@ -699,12 +731,15 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
CameraBottomControls( CameraBottomControls(
mainController: mc, mainController: mc,
isVideoRecording: mc.isVideoRecording, isVideoRecording: mc.isVideoRecording,
videoRecordingLocked: _videoRecordingLocked,
isFront: isFront, isFront: isFront,
keyTriggerButton: keyTriggerButton, keyTriggerButton: keyTriggerButton,
keyLockButton: keyLockButton,
onTakePicture: takePicture, onTakePicture: takePicture,
onPressSideButtonLeft: pressSideButtonLeft, onPressSideButtonLeft: pressSideButtonLeft,
onPressSideButtonRight: pressSideButtonRight, onPressSideButtonRight: pressSideButtonRight,
updateScaleFactor: updateScaleFactor, updateScaleFactor: updateScaleFactor,
onStopVideoRecording: () => stopVideoRecording(force: true),
), ),
VideoRecordingTimer( VideoRecordingTimer(
videoRecordingStarted: _videoRecordingStarted, videoRecordingStarted: _videoRecordingStarted,