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 # Changelog
## 0.2.17 ## 0.2.18
- New: Adds an "Ask a Friend" button to new contact suggestions. - New: Adds an "Ask a Friend" button to new contact suggestions.
- New: Adds security profiles. - New: Adds security profiles.
@ -10,6 +10,7 @@
- Fix: Issue with receiving messages when user closed app while decrypting - Fix: Issue with receiving messages when user closed app while decrypting
- Fix: Background message fetching reliability. - Fix: Background message fetching reliability.
- Fix: Issue with focus changing when taking a picture - Fix: Issue with focus changing when taking a picture
- Fix: Issues with the camera initialization
## 0.2.16 ## 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 allowErrorTrackingViaSentry = false;
static bool gotMessageFromServer = false; static bool gotMessageFromServer = false;
static int latestAppVersionId = 116; static int latestAppVersionId = 116;
static bool hasCameraPermissions = false;
} }

View file

@ -46,24 +46,43 @@ class ScreenshotController {
} }
late GlobalKey _containerKey; late GlobalKey _containerKey;
Future<ScreenshotImageHelper?> capture({double? pixelRatio}) async { Future<ScreenshotImageHelper?> capture({
double? pixelRatio,
int retries = 20,
}) async {
try { try {
final findRenderObject = _containerKey.currentContext?.findRenderObject(); final findRenderObject = _containerKey.currentContext?.findRenderObject();
if (findRenderObject == null) { if (findRenderObject == null) {
return null; return null;
} }
final boundary = findRenderObject as RenderRepaintBoundary; final boundary = findRenderObject as RenderRepaintBoundary;
final context = _containerKey.currentContext; final context = _containerKey.currentContext;
var tmpPixelRatio = pixelRatio; var tmpPixelRatio = pixelRatio;
if (tmpPixelRatio == null) { if (tmpPixelRatio == null) {
if (context != null && context.mounted) { if (context != null && context.mounted) {
tmpPixelRatio = tmpPixelRatio = tmpPixelRatio ?? MediaQuery.of(context).devicePixelRatio;
tmpPixelRatio ?? MediaQuery.of(context).devicePixelRatio;
} }
} }
final image = await boundary.toImage(pixelRatio: tmpPixelRatio ?? 1); final image = await boundary.toImage(pixelRatio: tmpPixelRatio ?? 1);
return ScreenshotImageHelper(image: image); return ScreenshotImageHelper(image: image);
} catch (e) { } 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); Log.error(e);
} }
return null; return null;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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