Fix: Issues with the camera initialization
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2026-05-21 16:12:19 +02:00
parent d6432677df
commit fae5ca3d25
21 changed files with 228 additions and 162 deletions

View file

@ -1,6 +1,6 @@
# Changelog
## 0.2.17
## 0.2.18
- New: Adds an "Ask a Friend" button to new contact suggestions.
- New: Adds security profiles.
@ -10,6 +10,7 @@
- Fix: Issue with receiving messages when user closed app while decrypting
- Fix: Background message fetching reliability.
- Fix: Issue with focus changing when taking a picture
- Fix: Issues with the camera initialization
## 0.2.16

2
fastlane/Appfile Normal file
View file

@ -0,0 +1,2 @@
json_key_file(ENV["GOOGLE_PLAY_JSON_KEY_PATH"] || "../../local_data/accesskeys/upload_track_releases_google_play.json")
package_name("eu.twonly") # Your application ID

15
fastlane/Fastfile Normal file
View file

@ -0,0 +1,15 @@
default_platform(:android)
platform :android do
desc "Submit a new App Bundle to the Google Play Internal Track"
lane :internal do
# This lane assumes that `flutter build appbundle` has already been run from the flutter root.
upload_to_play_store(
track: 'internal',
aab: 'build/app/outputs/bundle/release/app-release.aab',
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
end

32
fastlane/README.md Normal file
View file

@ -0,0 +1,32 @@
fastlane documentation
----
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```sh
xcode-select --install
```
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
# Available Actions
## Android
### android internal
```sh
[bundle exec] fastlane android internal
```
Submit a new App Bundle to the Google Play Internal Track
----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).

18
fastlane/report.xml Normal file
View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="fastlane.lanes">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000214">
</testcase>
<testcase classname="fastlane.lanes" name="1: upload_to_play_store" time="160.411287">
</testcase>
</testsuite>
</testsuites>

View file

@ -33,4 +33,5 @@ class AppState {
static bool allowErrorTrackingViaSentry = false;
static bool gotMessageFromServer = false;
static int latestAppVersionId = 116;
static bool hasCameraPermissions = false;
}

View file

@ -46,24 +46,43 @@ class ScreenshotController {
}
late GlobalKey _containerKey;
Future<ScreenshotImageHelper?> capture({double? pixelRatio}) async {
Future<ScreenshotImageHelper?> capture({
double? pixelRatio,
int retries = 20,
}) async {
try {
final findRenderObject = _containerKey.currentContext?.findRenderObject();
if (findRenderObject == null) {
return null;
}
final boundary = findRenderObject as RenderRepaintBoundary;
final context = _containerKey.currentContext;
var tmpPixelRatio = pixelRatio;
if (tmpPixelRatio == null) {
if (context != null && context.mounted) {
tmpPixelRatio =
tmpPixelRatio ?? MediaQuery.of(context).devicePixelRatio;
tmpPixelRatio = tmpPixelRatio ?? MediaQuery.of(context).devicePixelRatio;
}
}
final image = await boundary.toImage(pixelRatio: tmpPixelRatio ?? 1);
return ScreenshotImageHelper(image: image);
} catch (e) {
if (retries > 0) {
final completer = Completer<ScreenshotImageHelper?>();
WidgetsBinding.instance.addPostFrameCallback((_) async {
final result = await capture(
pixelRatio: pixelRatio,
retries: retries - 1,
);
completer.complete(result);
});
Timer(const Duration(milliseconds: 50), () {
if (!completer.isCompleted) {
WidgetsBinding.instance.scheduleFrame();
}
});
return completer.future;
}
Log.error(e);
}
return null;

View file

@ -203,6 +203,7 @@ class _StartNewChatView extends State<AddNewShortcutView> {
const SizedBox(width: 8),
],
),
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
floatingActionButton: FilledButton.icon(
onPressed: (_selectedGroups.isEmpty || shortcutEmoji == null)
? null

View file

@ -48,7 +48,7 @@ class SelectedCameraDetails {
bool cameraLoaded = false;
}
class CameraPreviewControllerView extends StatelessWidget {
class CameraPreviewControllerView extends StatefulWidget {
const CameraPreviewControllerView({
required this.mainController,
required this.isVisible,
@ -62,23 +62,52 @@ class CameraPreviewControllerView extends StatelessWidget {
final bool isVisible;
final bool hideControllers;
@override
State<CameraPreviewControllerView> createState() => _CameraPreviewControllerViewState();
}
class _CameraPreviewControllerViewState extends State<CameraPreviewControllerView> {
Future<bool>? _permissionsFuture;
@override
void initState() {
super.initState();
if (!AppState.hasCameraPermissions) {
_permissionsFuture = checkPermissions().then((hasPermission) {
if (hasPermission) {
AppState.hasCameraPermissions = true;
}
return hasPermission;
});
}
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: checkPermissions(),
if (AppState.hasCameraPermissions) {
return CameraPreviewView(
sendToGroup: widget.sendToGroup,
mainCameraController: widget.mainController,
isVisible: widget.isVisible,
hideControllers: widget.hideControllers,
);
}
return FutureBuilder<bool>(
future: _permissionsFuture,
builder: (context, snap) {
if (snap.hasData) {
if (snap.data!) {
return CameraPreviewView(
sendToGroup: sendToGroup,
mainCameraController: mainController,
isVisible: isVisible,
hideControllers: hideControllers,
sendToGroup: widget.sendToGroup,
mainCameraController: widget.mainController,
isVisible: widget.isVisible,
hideControllers: widget.hideControllers,
);
} else {
return PermissionHandlerView(
onSuccess: () {
mainController.selectCamera(0, true);
widget.mainController.selectCamera(0, true);
},
);
}
@ -210,8 +239,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
Future<void> initAsync() async {
_hasAudioPermission = await Permission.microphone.isGranted;
if (!_hasAudioPermission &&
!userService.currentUser.requestedAudioPermission) {
if (!_hasAudioPermission && !userService.currentUser.requestedAudioPermission) {
await UserService.update((u) => u.requestedAudioPermission = true);
await requestMicrophonePermission();
}
@ -232,8 +260,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
}
Future<void> updateScaleFactor(double newScale) async {
if (mc.selectedCameraDetails.scaleFactor == newScale ||
mc.cameraController == null) {
if (mc.selectedCameraDetails.scaleFactor == newScale || mc.cameraController == null) {
return;
}
await mc.cameraController?.setZoomLevel(
@ -316,9 +343,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
bool sharedFromGallery = false,
MediaType? mediaType,
}) async {
final type =
mediaType ??
((videoFilePath != null) ? MediaType.video : MediaType.image);
final type = mediaType ?? ((videoFilePath != null) ? MediaType.video : MediaType.image);
final mediaFileService = await initializeMediaUpload(
type,
userService.currentUser.defaultShowTime,
@ -359,8 +384,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
mainCameraController: mc,
previewLink: mc.sharedLinkForPreview,
),
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return child;
},
transitionDuration: Duration.zero,
@ -392,16 +416,13 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
return false;
}
bool get isFront =>
mc.cameraController?.description.lensDirection ==
CameraLensDirection.front;
bool get isFront => mc.cameraController?.description.lensDirection == CameraLensDirection.front;
Future<void> onPanUpdate(dynamic details) async {
if (details == null) {
return;
}
if (mc.cameraController == null ||
!mc.cameraController!.value.isInitialized) {
if (mc.cameraController == null || !mc.cameraController!.value.isInitialized) {
return;
}
@ -530,8 +551,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
}
Future<void> startVideoRecording() async {
if (mc.cameraController != null &&
mc.cameraController!.value.isRecordingVideo) {
if (mc.cameraController != null && mc.cameraController!.value.isRecordingVideo) {
return;
}
setState(() {
@ -551,8 +571,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
_currentTime = clock.now();
});
if (_videoRecordingStarted != null &&
_currentTime.difference(_videoRecordingStarted!).inSeconds >=
maxVideoRecordingTime) {
_currentTime.difference(_videoRecordingStarted!).inSeconds >= maxVideoRecordingTime) {
timer.cancel();
_videoRecordingTimer = null;
stopVideoRecording();
@ -589,8 +608,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
_videoRecordingLocked = false;
});
if (mc.cameraController == null ||
!mc.cameraController!.value.isRecordingVideo) {
if (mc.cameraController == null || !mc.cameraController!.value.isRecordingVideo) {
return;
}
@ -618,8 +636,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
@override
Widget build(BuildContext context) {
if (mc.selectedCameraDetails.cameraId >= AppEnvironment.cameras.length ||
mc.cameraController == null) {
if (mc.selectedCameraDetails.cameraId >= AppEnvironment.cameras.length || mc.cameraController == null) {
return Container();
}
return StreamBuilder(
@ -643,9 +660,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
_baseScaleFactor = mc.selectedCameraDetails.scaleFactor;
});
// Get the position of the pointer
final renderBox =
keyTriggerButton.currentContext!.findRenderObject()!
as RenderBox;
final renderBox = keyTriggerButton.currentContext!.findRenderObject()! as RenderBox;
final localPosition = renderBox.globalToLocal(
details.globalPosition,
);
@ -681,24 +696,18 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
),
),
),
if (!mc.isSharePreviewIsShown &&
widget.sendToGroup != null &&
!mc.isVideoRecording)
if (!mc.isSharePreviewIsShown && widget.sendToGroup != null && !mc.isVideoRecording)
ShowTitleText(
title: widget.sendToGroup!.groupName,
desc: context.lang.cameraPreviewSendTo,
),
if (!mc.isSharePreviewIsShown &&
mc.sharedLinkForPreview != null &&
!mc.isVideoRecording)
if (!mc.isSharePreviewIsShown && mc.sharedLinkForPreview != null && !mc.isVideoRecording)
ShowTitleText(
title: mc.sharedLinkForPreview?.host ?? '',
desc: 'Link',
isLink: true,
),
if (!mc.isSharePreviewIsShown &&
!mc.isVideoRecording &&
!widget.hideControllers)
if (!mc.isSharePreviewIsShown && !mc.isVideoRecording && !widget.hideControllers)
CameraTopActions(
selectedCameraDetails: mc.selectedCameraDetails,
hasAudioPermission: _hasAudioPermission,
@ -742,8 +751,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
videoRecordingStarted: _videoRecordingStarted,
maxVideoRecordingTime: maxVideoRecordingTime,
),
if (!mc.isSharePreviewIsShown && widget.sendToGroup != null ||
widget.hideControllers)
if (!mc.isSharePreviewIsShown && widget.sendToGroup != null || widget.hideControllers)
Positioned(
left: 5,
top: 10,

View file

@ -87,8 +87,10 @@ class MainCameraController {
Future<void>? _initializeFuture;
Future<void>? _pendingDisposal;
int _cameraSessionId = 0;
Future<void> closeCamera() async {
_cameraSessionId++;
contactsVerified = {};
scannedNewProfiles = {};
scannedUrl = null;
@ -119,11 +121,14 @@ class MainCameraController {
}
Future<void> selectCamera(int sCameraId, bool init) async {
await _pendingDisposal;
initCameraStarted = true;
final sessionId = ++_cameraSessionId;
await _pendingDisposal;
if (sessionId != _cameraSessionId) return;
if (AppEnvironment.cameras.isEmpty) {
AppEnvironment.cameras = await availableCameras();
if (sessionId != _cameraSessionId) return;
}
var cameraId = sCameraId;
@ -145,10 +150,13 @@ class MainCameraController {
selectedCameraDetails.isZoomAble = false;
if (cameraController == null) {
final hasMic = await Permission.microphone.isGranted;
if (sessionId != _cameraSessionId) return;
cameraController = CameraController(
AppEnvironment.cameras[cameraId],
ResolutionPreset.high,
enableAudio: await Permission.microphone.isGranted,
enableAudio: hasMic,
imageFormatGroup: Platform.isAndroid ? ImageFormatGroup.nv21 : ImageFormatGroup.bgra8888,
);
try {

View file

@ -8,7 +8,15 @@ import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart';
class CameraZoomButtons extends StatefulWidget {
String beautifulZoomScale(double scale) {
var tmp = scale.toStringAsFixed(1);
if (tmp[0] == '0') {
tmp = tmp.substring(1, tmp.length);
}
return tmp;
}
class CameraZoomButtons extends StatelessWidget {
const CameraZoomButtons({
required this.controller,
required this.updateScaleFactor,
@ -25,32 +33,10 @@ class CameraZoomButtons extends StatefulWidget {
final Future<void> Function(int sCameraId, bool init) selectCamera;
@override
State<CameraZoomButtons> createState() => _CameraZoomButtonsState();
}
String beautifulZoomScale(double scale) {
var tmp = scale.toStringAsFixed(1);
if (tmp[0] == '0') {
tmp = tmp.substring(1, tmp.length);
}
return tmp;
}
class _CameraZoomButtonsState extends State<CameraZoomButtons> {
bool showWideAngleZoom = false;
bool showWideAngleZoomIOS = false;
bool _isDisposed = false;
int? _wideCameraIndex;
@override
void initState() {
super.initState();
unawaited(initAsync());
}
Future<void> initAsync() async {
showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1;
Widget build(BuildContext context) {
final showWideAngleZoom = selectedCameraDetails.minAvailableZoom < 1;
int? wideCameraIndex;
var index = AppEnvironment.cameras.indexWhere(
(t) => t.lensType == CameraLensType.ultraWide,
);
@ -60,33 +46,13 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
);
}
if (index != -1) {
_wideCameraIndex = index;
wideCameraIndex = index;
}
final isFront =
widget.controller.description.lensDirection ==
CameraLensDirection.front;
final isFront = controller.description.lensDirection == CameraLensDirection.front;
if (!showWideAngleZoom &&
Platform.isIOS &&
_wideCameraIndex != null &&
!isFront) {
showWideAngleZoomIOS = true;
} else {
showWideAngleZoomIOS = false;
}
if (_isDisposed) return;
setState(() {});
}
final showWideAngleZoomIOS = !showWideAngleZoom && Platform.isIOS && wideCameraIndex != null && !isFront;
@override
void dispose() {
_isDisposed = true; // Set the flag to true when disposing
super.dispose();
}
@override
Widget build(BuildContext context) {
final zoomButtonStyle = TextButton.styleFrom(
padding: EdgeInsets.zero,
foregroundColor: Colors.white,
@ -97,24 +63,21 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
const zoomTextStyle = TextStyle(fontSize: 13);
final isSmallerFocused =
widget.scaleFactor < 1 ||
(showWideAngleZoomIOS &&
widget.selectedCameraDetails.cameraId == _wideCameraIndex);
scaleFactor < 1 || (showWideAngleZoomIOS && selectedCameraDetails.cameraId == wideCameraIndex);
final isMiddleFocused =
widget.scaleFactor >= 1 &&
widget.scaleFactor < 2 &&
!(showWideAngleZoomIOS &&
widget.selectedCameraDetails.cameraId == _wideCameraIndex);
scaleFactor >= 1 &&
scaleFactor < 2 &&
!(showWideAngleZoomIOS && selectedCameraDetails.cameraId == wideCameraIndex);
final maxLevel = max(
min(widget.selectedCameraDetails.maxAvailableZoom, 2),
widget.scaleFactor,
min(selectedCameraDetails.maxAvailableZoom, 2),
scaleFactor,
);
final minLevel = beautifulZoomScale(
widget.selectedCameraDetails.minAvailableZoom,
selectedCameraDetails.minAvailableZoom,
);
final currentLevel = beautifulZoomScale(widget.scaleFactor);
final currentLevel = beautifulZoomScale(scaleFactor);
return Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(40),
@ -132,20 +95,18 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
),
onPressed: () async {
if (showWideAngleZoomIOS) {
if (_wideCameraIndex != null) {
await widget.selectCamera(_wideCameraIndex!, true);
if (wideCameraIndex != null) {
await selectCamera(wideCameraIndex, true);
}
} else {
final level = await widget.controller.getMinZoomLevel();
widget.updateScaleFactor(level);
final level = await controller.getMinZoomLevel();
updateScaleFactor(level);
}
},
child: showWideAngleZoomIOS
? const Text('0.5')
: Text(
widget.scaleFactor < 1
? '${currentLevel}x'
: '${minLevel}x',
scaleFactor < 1 ? '${currentLevel}x' : '${minLevel}x',
style: zoomTextStyle,
),
),
@ -156,39 +117,33 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
),
),
onPressed: () async {
if (showWideAngleZoomIOS &&
widget.selectedCameraDetails.cameraId ==
_wideCameraIndex) {
await widget.selectCamera(0, true);
if (showWideAngleZoomIOS && selectedCameraDetails.cameraId == wideCameraIndex) {
await selectCamera(0, true);
} else {
widget.updateScaleFactor(1.0);
updateScaleFactor(1.0);
}
},
child: Text(
isMiddleFocused
? '${beautifulZoomScale(widget.scaleFactor)}x'
: '1.0x',
isMiddleFocused ? '${beautifulZoomScale(scaleFactor)}x' : '1.0x',
style: zoomTextStyle,
),
),
TextButton(
style: zoomButtonStyle.copyWith(
foregroundColor: WidgetStateProperty.all(
(widget.scaleFactor >= 2) ? Colors.yellow : Colors.white,
(scaleFactor >= 2) ? Colors.yellow : Colors.white,
),
),
onPressed: () async {
final level = min(
await widget.controller.getMaxZoomLevel(),
await controller.getMaxZoomLevel(),
2,
).toDouble();
if (showWideAngleZoomIOS &&
widget.selectedCameraDetails.cameraId ==
_wideCameraIndex) {
await widget.selectCamera(0, true);
if (showWideAngleZoomIOS && selectedCameraDetails.cameraId == wideCameraIndex) {
await selectCamera(0, true);
}
widget.updateScaleFactor(level);
updateScaleFactor(level);
},
child: Text(
'${beautifulZoomScale(maxLevel.toDouble())}x',

View file

@ -254,6 +254,7 @@ class _ShareImageView extends State<ShareImageView> {
),
),
),
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
floatingActionButton: _allGroups.isEmpty
? null
: SizedBox(

View file

@ -156,7 +156,7 @@ class UserCheckbox extends StatelessWidget {
Row(
children: [
Text(
substringBy(group.groupName, 12),
substringBy(group.groupName, 11),
overflow: TextOverflow.ellipsis,
),
],

View file

@ -214,8 +214,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
List<Widget> get actionsAtTheRight {
if (layers.isNotEmpty &&
(layers.first.isEditing ||
(layers.last.isEditing && layers.last.hasCustomActionButtons))) {
(layers.first.isEditing || (layers.last.isEditing && layers.last.hasCustomActionButtons))) {
return [];
}
return <Widget>[
@ -291,13 +290,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
if (media.type == MediaType.video) ...[
const SizedBox(height: 8),
ActionButton(
(mediaService.removeAudio)
? Icons.volume_off_rounded
: Icons.volume_up_rounded,
(mediaService.removeAudio) ? Icons.volume_off_rounded : Icons.volume_up_rounded,
tooltipText: 'Enable Audio in Video',
color: (mediaService.removeAudio)
? Colors.white.withAlpha(160)
: Colors.white,
color: (mediaService.removeAudio) ? Colors.white.withAlpha(160) : Colors.white,
onPressed: () async {
await mediaService.toggleRemoveAudio();
if (mediaService.removeAudio) {
@ -335,9 +330,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
ActionButton(
FontAwesomeIcons.shieldHeart,
tooltipText: context.lang.protectAsARealTwonly,
color: media.requiresAuthentication
? Theme.of(context).colorScheme.primary
: Colors.white,
color: media.requiresAuthentication ? Theme.of(context).colorScheme.primary : Colors.white,
onPressed: () async {
await mediaService.setRequiresAuth(!media.requiresAuthentication);
selectedGroupIds = HashSet();
@ -383,8 +376,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
List<Widget> get actionsAtTheTop {
if (layers.isNotEmpty &&
(layers.first.isEditing ||
(layers.last.isEditing && layers.last.hasCustomActionButtons))) {
(layers.first.isEditing || (layers.last.isEditing && layers.last.hasCustomActionButtons))) {
return [];
}
return [
@ -474,6 +466,14 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
return (layers.first as BackgroundLayerData).image.image;
}
}
if (layers.length == 2) {
final filterLayer = layers[1];
if (layers.first is BackgroundLayerData && filterLayer is FilterLayerData) {
if (filterLayer.page == 1) {
return (layers.first as BackgroundLayerData).image.image;
}
}
}
for (final x in layers) {
x.showCustomButtons = false;
@ -513,15 +513,15 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
}
}
ScreenshotImageHelper? image;
var bytes = await widget.screenshotImage?.getBytes();
if (media.type == MediaType.gif) {
final bytes = await widget.screenshotImage?.getBytes();
if (bytes != null) {
mediaService.originalPath.writeAsBytesSync(bytes.toList());
}
} else {
image = await getEditedImageBytes();
if (image == null) return null;
bytes = await image.getBytes();
final bytes = await image.getBytes();
if (bytes == null) {
Log.error('imageBytes are empty');
return null;
@ -657,9 +657,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
await askToCloseThenClose();
},
child: Scaffold(
backgroundColor: widget.sharedFromGallery
? null
: Colors.white.withAlpha(0),
backgroundColor: widget.sharedFromGallery ? null : Colors.white.withAlpha(0),
resizeToAvoidBottomInset: false,
body: Stack(
fit: StackFit.expand,

View file

@ -283,6 +283,7 @@ class _ChatListViewState extends State<ChatListView> {
],
),
),
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
floatingActionButton: !_hasContacts
? null
: Padding(

View file

@ -58,6 +58,7 @@ class _GroupCreateSelectGroupNameViewState
appBar: AppBar(
title: Text(context.lang.selectGroupName),
),
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
floatingActionButton: FilledButton.icon(
onPressed: (textFieldGroupName.text.isEmpty || _isLoading)
? null

View file

@ -129,6 +129,7 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
: context.lang.addMember,
),
),
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
floatingActionButton: FilledButton.icon(
onPressed: selectedUsers.isEmpty ? null : submitChanges,
label: Text(

View file

@ -267,15 +267,17 @@ class HomeViewState extends State<HomeView> {
),
),
),
if (_offsetRatio == 0)
Positioned.fill(
child: GestureDetector(
child: _offsetRatio == 0
? GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTap: _mainCameraController.onDoubleTap,
onTapDown: _mainCameraController.onTapDown,
),
)
: const SizedBox.shrink(),
),
Positioned(
key: const ValueKey('camera_controls'),
left: 0,
top: 0,
right: 0,

View file

@ -99,6 +99,7 @@ class _SelectAdditionalUsers extends State<SelectAdditionalUsers> {
appBar: AppBar(
title: Text(context.lang.additionalUserSelectTitle),
),
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
floatingActionButton: FilledButton.icon(
onPressed: selectedUsers.isEmpty
? null

View file

@ -112,6 +112,7 @@ class _SelectAdditionalUsers extends State<SelectContactsView> {
appBar: AppBar(
title: Text(widget.text.title),
),
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
floatingActionButton: FilledButton.icon(
onPressed: selectedUsers.isEmpty
? null

View file

@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
publish_to: 'none'
version: 0.2.16+125
version: 0.2.17+126
environment:
sdk: ^3.11.0