mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 03:42: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: 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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue