mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 05:22:13 +00:00
Improved: Lock to record hands-free
This commit is contained in:
parent
b2e9b04659
commit
51477e3f51
3 changed files with 145 additions and 3 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue