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: FAQ is now in the app rather than opening in the browser
- Improved: Videos can now be paused
- Improved: Lock to record hands-free
- Fix: Many smaller issues
## 0.1.8

View file

@ -9,23 +9,29 @@ class CameraBottomControls extends StatelessWidget {
const CameraBottomControls({
required this.mainController,
required this.isVideoRecording,
required this.videoRecordingLocked,
required this.isFront,
required this.keyTriggerButton,
required this.keyLockButton,
required this.onTakePicture,
required this.onPressSideButtonLeft,
required this.onPressSideButtonRight,
required this.updateScaleFactor,
required this.onStopVideoRecording,
super.key,
});
final MainCameraController mainController;
final bool isVideoRecording;
final bool videoRecordingLocked;
final bool isFront;
final GlobalKey keyTriggerButton;
final GlobalKey keyLockButton;
final VoidCallback onTakePicture;
final VoidCallback onPressSideButtonLeft;
final VoidCallback onPressSideButtonRight;
final Future<void> Function(double) updateScaleFactor;
final VoidCallback onStopVideoRecording;
MainCameraController get mc => mainController;
@ -57,13 +63,18 @@ class CameraBottomControls extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (!isVideoRecording) _buildSideButtonLeft(),
if (!isVideoRecording)
_buildSideButtonLeft()
else
_buildLockOrStopButton(),
_buildShutterButton(),
if (!isVideoRecording)
if (isFront)
_buildSideButtonRight()
else
const SizedBox(width: 80),
const SizedBox(width: 80)
else
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() {
return GestureDetector(
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;
DateTime? _videoRecordingStarted;
Timer? _videoRecordingTimer;
bool _videoRecordingLocked = false;
DateTime _currentTime = clock.now();
final GlobalKey keyTriggerButton = GlobalKey();
final GlobalKey keyLockButton = GlobalKey();
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
MainCameraController get mc => widget.mainCameraController;
@ -399,6 +401,32 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
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(() {
mc.selectedCameraDetails.scaleFactor =
(_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) {
_videoRecordingTimer?.cancel();
_videoRecordingTimer = null;
@ -548,6 +579,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
setState(() {
_videoRecordingStarted = null;
mc.isVideoRecording = false;
_videoRecordingLocked = false;
});
if (mc.cameraController == null ||
@ -699,12 +731,15 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
CameraBottomControls(
mainController: mc,
isVideoRecording: mc.isVideoRecording,
videoRecordingLocked: _videoRecordingLocked,
isFront: isFront,
keyTriggerButton: keyTriggerButton,
keyLockButton: keyLockButton,
onTakePicture: takePicture,
onPressSideButtonLeft: pressSideButtonLeft,
onPressSideButtonRight: pressSideButtonRight,
updateScaleFactor: updateScaleFactor,
onStopVideoRecording: () => stopVideoRecording(force: true),
),
VideoRecordingTimer(
videoRecordingStarted: _videoRecordingStarted,