switch back to old camera plugin #28

This commit is contained in:
otsmr 2025-03-17 00:06:38 +01:00
parent 644e5125d8
commit 9c027bd924
7 changed files with 393 additions and 457 deletions

View file

@ -1,3 +1,4 @@
import 'package:camera/camera.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/providers/api_provider.dart';
@ -5,3 +6,5 @@ late ApiProvider apiProvider;
// uses for background notification
late TwonlyDatabase twonlyDatabase;
List<CameraDescription> gCameras = <CameraDescription>[];

View file

@ -1,3 +1,4 @@
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
@ -37,6 +38,8 @@ void main() async {
await initMediaStorage();
await initFCMService();
gCameras = await availableCameras();
apiProvider = ApiProvider();
twonlyDatabase = TwonlyDatabase();

View file

@ -1,194 +1,108 @@
import 'package:camerawesome/camerawesome_plugin.dart';
import 'dart:math';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
class ZoomSelector extends StatefulWidget {
final CameraState state;
class CameraZoomButtons extends StatefulWidget {
const CameraZoomButtons(
{super.key,
required this.controller,
required this.updateScaleFactor,
required this.scaleFactor});
const ZoomSelector({
super.key,
required this.state,
});
final CameraController controller;
final double scaleFactor;
final Function updateScaleFactor;
@override
State<ZoomSelector> createState() => _ZoomSelectorState();
State<CameraZoomButtons> createState() => _CameraZoomButtonsState();
}
class _ZoomSelectorState extends State<ZoomSelector> {
double? minZoom;
double? maxZoom;
@override
void initState() {
super.initState();
initAsync();
}
initAsync() async {
minZoom = await CamerawesomePlugin.getMinZoom();
maxZoom = await CamerawesomePlugin.getMaxZoom();
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> {
@override
Widget build(BuildContext context) {
return StreamBuilder<SensorConfig>(
stream: widget.state.sensorConfig$,
builder: (context, sensorConfigSnapshot) {
initAsync();
if (sensorConfigSnapshot.data == null ||
minZoom == null ||
maxZoom == null) {
return const SizedBox.shrink();
}
var zoomButtonStyle = TextButton.styleFrom(
padding: EdgeInsets.zero,
foregroundColor: Colors.white,
minimumSize: Size(40, 40),
alignment: Alignment.center,
tapTargetSize: MaterialTapTargetSize.shrinkWrap);
return StreamBuilder<double>(
stream: sensorConfigSnapshot.requireData.zoom$,
builder: (context, snapshot) {
if (snapshot.hasData) {
return _ZoomIndicatorLayout(
zoom: snapshot.requireData,
min: minZoom!,
max: maxZoom!,
sensorConfig: widget.state.sensorConfig,
);
} else {
return const SizedBox.shrink();
}
},
);
},
);
}
}
class _ZoomIndicatorLayout extends StatelessWidget {
final double zoom;
final double min;
final double max;
final SensorConfig sensorConfig;
const _ZoomIndicatorLayout({
required this.zoom,
required this.min,
required this.max,
required this.sensorConfig,
});
@override
Widget build(BuildContext context) {
final displayZoom = (max - min) * zoom + min;
if (min == 1.0) {
return Container();
}
return Row(
final zoomTextStyle = TextStyle(fontSize: 13);
return Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(40.0),
child: Container(
color: const Color.fromARGB(90, 0, 0, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Show 3 dots for zooming: min, 1.0X and max zoom. The closer one shows
// text, the other ones a dot.
_ZoomIndicator(
normalValue: 0.0,
zoom: zoom,
selected: displayZoom < 1.0,
min: min,
max: max,
sensorConfig: sensorConfig,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: _ZoomIndicator(
normalValue: 0.5,
zoom: zoom,
selected: !(displayZoom < 1.0 || displayZoom == max),
min: min,
max: max,
sensorConfig: sensorConfig,
),
),
_ZoomIndicator(
normalValue: 1.0,
zoom: zoom,
selected: displayZoom == max,
min: min,
max: max,
sensorConfig: sensorConfig,
),
],
TextButton(
style: zoomButtonStyle,
onPressed: () async {
var level = await widget.controller.getMinZoomLevel();
widget.updateScaleFactor(level);
},
child: FutureBuilder(
future: widget.controller.getMinZoomLevel(),
builder: (context, snap) {
if (snap.hasData) {
var minLevel =
beautifulZoomScale(snap.data!.toDouble());
var currentLevel =
beautifulZoomScale(widget.scaleFactor);
return Text(
widget.scaleFactor < 1
? "${currentLevel}x"
: "${minLevel}x",
style: zoomTextStyle,
);
} else {
return Text("");
}
}
class _ZoomIndicator extends StatelessWidget {
final double zoom;
final double min;
final double max;
final double normalValue;
final SensorConfig sensorConfig;
final bool selected;
const _ZoomIndicator({
required this.zoom,
required this.min,
required this.max,
required this.normalValue,
required this.sensorConfig,
required this.selected,
});
@override
Widget build(BuildContext context) {
final baseTheme = AwesomeThemeProvider.of(context).theme;
final baseButtonTheme = baseTheme.buttonTheme;
final displayZoom = (max - min) * zoom + min;
Widget content = AnimatedSwitcher(
duration: const Duration(milliseconds: 100),
transitionBuilder: (child, anim) {
return ScaleTransition(scale: anim, child: child);
}),
),
TextButton(
style: zoomButtonStyle,
onPressed: () {
widget.updateScaleFactor(1.0);
},
child: selected
? AwesomeBouncingWidget(
key: ValueKey("zoomIndicator_${normalValue}_selected"),
onTap: () {
sensorConfig.setZoom(normalValue);
},
child: Container(
color: Colors.transparent,
padding: const EdgeInsets.all(0.0),
child: AwesomeCircleWidget(
theme: baseTheme,
child: Text(
"${displayZoom.toStringAsFixed(1)}X",
style: const TextStyle(color: Colors.white, fontSize: 12),
),
),
),
)
: AwesomeBouncingWidget(
key: ValueKey("zoomIndicator_${normalValue}_unselected"),
onTap: () {
sensorConfig.setZoom(normalValue);
(widget.scaleFactor >= 1 && widget.scaleFactor < 2)
? "${beautifulZoomScale(widget.scaleFactor)}x"
: "1.0x",
style: zoomTextStyle,
)),
TextButton(
style: zoomButtonStyle,
onPressed: () async {
var level = min(await widget.controller.getMaxZoomLevel(), 2)
.toDouble();
widget.updateScaleFactor(level);
},
child: Container(
color: Colors.transparent,
padding: const EdgeInsets.all(16.0),
child: AwesomeCircleWidget(
theme: baseTheme.copyWith(
buttonTheme: baseButtonTheme.copyWith(
backgroundColor: baseButtonTheme.foregroundColor,
padding: EdgeInsets.zero,
),
),
child: const SizedBox(width: 6, height: 6),
child: FutureBuilder(
future: widget.controller.getMaxZoomLevel(),
builder: (context, snap) {
if (snap.hasData) {
var maxLevel = min((snap.data?.toInt())!, 2);
return Text("${maxLevel}x", style: zoomTextStyle);
} else {
return Text("");
}
}),
)
],
),
),
),
);
// Same width for each dot to keep them in their position
return SizedBox(
width: 56,
child: Center(
child: content,
),
);
}
}

View file

@ -1,9 +1,9 @@
import 'dart:io';
import 'dart:ui';
import 'dart:typed_data';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:camerawesome/camerawesome_plugin.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/components/zoom_selector.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/components/image_editor/action_button.dart';
@ -48,13 +48,77 @@ class CameraPreviewView extends StatefulWidget {
}
class _CameraPreviewViewState extends State<CameraPreviewView> {
double _lastZoom = 1;
double _basePanY = 0;
double scaleFactor = 1;
bool sharePreviewIsShown = false;
bool isFlashOn = false;
bool showSelfieFlash = false;
int cameraId = 0;
bool isZoomAble = false;
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
late CameraController controller;
ScreenshotController screenshotController = ScreenshotController();
@override
void initState() {
super.initState();
selectCamera(0, init: true);
}
void selectCamera(int sCameraId, {bool init = false}) {
if (sCameraId > gCameras.length) return;
setState(() {
isZoomAble = false;
});
controller = CameraController(gCameras[sCameraId], ResolutionPreset.high);
controller.initialize().then((_) async {
if (!mounted) {
return;
}
controller.setFlashMode(isFlashOn ? FlashMode.always : FlashMode.off);
isZoomAble = await controller.getMinZoomLevel() !=
await controller.getMaxZoomLevel();
setState(() {});
}).catchError((Object e) {
if (e is CameraException) {
switch (e.code) {
case 'CameraAccessDenied':
// Handle access errors here.
break;
default:
// Handle other errors here.
break;
}
}
});
setState(() {
cameraId = sCameraId;
});
}
Future<void> updateScaleFactor(double newScale) async {
var minFactor = await controller.getMinZoomLevel();
var maxFactor = await controller.getMaxZoomLevel();
if (newScale < minFactor) {
newScale = minFactor;
}
if (newScale > maxFactor) {
newScale = maxFactor;
}
await controller.setZoomLevel(newScale);
setState(() {
scaleFactor = newScale;
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MediaViewSizing(
@ -62,117 +126,57 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
children: [
ClipRRect(
borderRadius: BorderRadius.circular(22),
child: CameraAwesomeBuilder.custom(
previewAlignment: Alignment.topLeft,
sensorConfig: SensorConfig.single(
aspectRatio: CameraAspectRatios.ratio_16_9,
zoom: 0.5,
),
previewFit: CameraPreviewFit.contain,
progressIndicator: Container(),
onMediaCaptureEvent: (event) {
switch ((event.status, event.isPicture, event.isVideo)) {
case (MediaCaptureStatus.capturing, true, false):
debugPrint('Capturing picture...');
case (MediaCaptureStatus.success, true, false):
event.captureRequest.when(
single: (single) async {
final imageBytes = await single.file?.readAsBytes();
if (imageBytes == null || !context.mounted) return;
debugPrint("Delete ${single.path!}");
File(single.path!).delete();
setState(() {
sharePreviewIsShown = true;
});
await Navigator.push(
context,
PageRouteBuilder(
opaque: false,
pageBuilder: (context, a1, a2) =>
ShareImageEditorView(imageBytes: imageBytes),
transitionsBuilder: (context, animation,
secondaryAnimation, child) {
return child;
},
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
),
);
if (context.mounted) {
setState(() {
sharePreviewIsShown = false;
});
}
},
multiple: (multiple) {
multiple.fileBySensor.forEach((key, value) {
debugPrint(
'multiple image taken: $key ${value?.path}');
});
},
);
case (MediaCaptureStatus.failure, true, false):
debugPrint('Failed to capture picture: ${event.exception}');
case (MediaCaptureStatus.capturing, false, true):
debugPrint('Capturing video...');
case (MediaCaptureStatus.success, false, true):
event.captureRequest.when(
single: (single) {
debugPrint('Video saved: ${single.file?.path}');
},
multiple: (multiple) {
multiple.fileBySensor.forEach((key, value) {
debugPrint(
'multiple video taken: $key ${value?.path}');
});
},
);
case (MediaCaptureStatus.failure, false, true):
debugPrint('Failed to capture video: ${event.exception}');
default:
debugPrint('Unknown event: $event');
}
},
builder: (cameraState, preview) {
return Stack(
child: Stack(
children: [
(controller.value.isInitialized)
? Positioned.fill(
child: Screenshot(
controller: screenshotController,
child: AspectRatio(
aspectRatio: 9 / 16,
child: ClipRect(
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: controller.value.previewSize!.height,
height: controller.value.previewSize!.width,
child: CameraPreview(controller),
),
),
),
)),
)
: Container(),
Positioned.fill(
child: GestureDetector(
onPanStart: (details) async {
if (cameraState.sensorConfig.sensors.first.position ==
SensorPosition.front) {
return;
}
setState(() {
_basePanY = details.localPosition.dy;
});
// if (cameraState.sensorConfig.sensors.first.position ==
// SensorPosition.front) {
// return;
// }
// setState(() {
// _basePanY = details.localPosition.dy;
// });
},
onPanUpdate: (details) async {
if (cameraState.sensorConfig.sensors.first.position ==
SensorPosition.front) {
return;
}
var diff = _basePanY - details.localPosition.dy;
if (diff > 200) diff = 200;
if (diff < 0) diff = 0;
var tmp = (diff / 200 * 50).toInt() / 50;
if (tmp != _lastZoom) {
cameraState.sensorConfig.setZoom(tmp);
setState(() {
(tmp);
_lastZoom = tmp;
});
}
// if (cameraState.sensorConfig.sensors.first.position ==
// SensorPosition.front) {
// return;
// }
// var diff = _basePanY - details.localPosition.dy;
// if (diff > 200) diff = 200;
// if (diff < 0) diff = 0;
// var tmp = (diff / 200 * 50).toInt() / 50;
// if (tmp != _lastZoom) {
// cameraState.sensorConfig.setZoom(tmp);
// setState(() {
// (tmp);
// _lastZoom = tmp;
// });
// }
},
onDoubleTap: () async {
bool isFront =
cameraState.sensorConfig.sensors.first.position ==
SensorPosition.front;
cameraState.switchCameraSensor(
aspectRatio: CameraAspectRatios.ratio_16_9,
flash: isFlashOn ? FlashMode.on : FlashMode.none,
zoom: isFront ? 0.5 : 0,
);
selectCamera((cameraId + 1) % 2);
},
),
),
@ -192,13 +196,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
tooltipText:
context.lang.switchFrontAndBackCamera,
onPressed: () async {
cameraState.switchCameraSensor(
aspectRatio:
CameraAspectRatios.ratio_16_9,
flash: isFlashOn
? FlashMode.on
: FlashMode.none,
);
selectCamera((cameraId + 1) % 2);
},
),
ActionButton(
@ -206,16 +204,13 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
tooltipText: context.lang.toggleFlashLight,
color: isFlashOn
? const Color.fromARGB(255, 255, 230, 0)
: const Color.fromARGB(
158, 255, 255, 255),
: const Color.fromARGB(158, 255, 255, 255),
onPressed: () async {
if (isFlashOn) {
cameraState.sensorConfig
.setFlashMode(FlashMode.none);
controller.setFlashMode(FlashMode.off);
isFlashOn = false;
} else {
cameraState.sensorConfig
.setFlashMode(FlashMode.on);
controller.setFlashMode(FlashMode.always);
isFlashOn = true;
}
setState(() {});
@ -235,24 +230,71 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
alignment: Alignment.bottomCenter,
child: Column(
children: [
ZoomSelector(state: cameraState),
if (controller.value.isInitialized &&
isZoomAble &&
controller.description.lensDirection !=
CameraLensDirection.front)
SizedBox(
width: 150,
child: CameraZoomButtons(
key: widget.key,
scaleFactor: scaleFactor,
updateScaleFactor: updateScaleFactor,
controller: controller,
),
),
const SizedBox(height: 30),
GestureDetector(
onTap: () async {
if (cameraState.sensorConfig.flashMode ==
FlashMode.on &&
cameraState.sensorConfig.sensors.first
.position ==
SensorPosition.front) {
if (isFlashOn) {
if (controller.description.lensDirection ==
CameraLensDirection.front) {
setState(() {
showSelfieFlash = true;
});
await Future.delayed(
Duration(milliseconds: 500));
} else {
controller.setFlashMode(FlashMode.torch);
}
cameraState.when(
onPhotoMode: (picState) =>
picState.takePhoto());
await Future.delayed(
Duration(milliseconds: 1000));
}
await controller.pausePreview();
if (!context.mounted) return;
controller.setFlashMode(
isFlashOn ? FlashMode.always : FlashMode.off);
Future<Uint8List?> imageBytes =
screenshotController.capture(pixelRatio: 1);
setState(() {
sharePreviewIsShown = true;
});
await Navigator.push(
context,
PageRouteBuilder(
opaque: false,
pageBuilder: (context, a1, a2) =>
ShareImageEditorView(
imageBytes: imageBytes),
transitionsBuilder: (context, animation,
secondaryAnimation, child) {
return child;
},
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
),
);
// does not work??
//await controller.resumePreview();
selectCamera(0);
if (context.mounted) {
setState(() {
sharePreviewIsShown = false;
});
}
setState(() {
showSelfieFlash = false;
});
@ -280,19 +322,6 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
),
),
],
);
},
saveConfig: SaveConfig.photoAndVideo(
photoPathBuilder: (sensors) async {
final Directory extDir = await getTemporaryDirectory();
final testDir = await Directory(
'${extDir.path}/images',
).create(recursive: true);
final String filePath =
'${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.jpg';
return SingleCaptureRequest(filePath, sensors.first);
},
),
),
),
if (showSelfieFlash)
@ -304,18 +333,6 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
),
),
),
if (sharePreviewIsShown)
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 100.0,
sigmaY: 100.0,
),
child: Center(
child: CircularProgressIndicator(),
),
),
)
],
),
);

View file

@ -26,7 +26,7 @@ List<Layer> removedLayers = [];
class ShareImageEditorView extends StatefulWidget {
const ShareImageEditorView({super.key, required this.imageBytes});
final Uint8List imageBytes;
final Future<Uint8List?> imageBytes;
@override
State<ShareImageEditorView> createState() => _ShareImageEditorView();
}
@ -223,8 +223,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
return image;
}
Future<void> loadImage(dynamic imageFile) async {
await currentImage.load(imageFile);
Future<void> loadImage(Future<Uint8List?> imageFile) async {
Uint8List? imageBytes = await imageFile;
await currentImage.load(imageBytes);
layers.clear();

View file

@ -129,22 +129,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.9.3"
camerawesome:
camera:
dependency: "direct main"
description:
name: camerawesome
sha256: "3619d5605fb14ab72c815532c1d9f635512c75df07b5a742b60a9a4b03b6081e"
name: camera
sha256: "413d2b34fe28496c35c69ede5b232fb9dd5ca2c3a4cb606b14efc1c7546cc8cb"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
carousel_slider:
version: "0.11.1"
camera_android_camerax:
dependency: transitive
description:
name: carousel_slider
sha256: "7b006ec356205054af5beaef62e2221160ea36b90fb70a35e4deacd49d0349ae"
name: camera_android_camerax
sha256: "13784f539c7f104766bff84e4479a70f03b29d78b208278be45c939250d9d7f5"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
version: "0.6.14+1"
camera_avfoundation:
dependency: transitive
description:
name: camera_avfoundation
sha256: "3057ada0b30402e3a9b6dffec365c9736a36edbf04abaecc67c4309eadc86b49"
url: "https://pub.dev"
source: hosted
version: "0.9.18+9"
camera_platform_interface:
dependency: transitive
description:
name: camera_platform_interface
sha256: "953e7baed3a7c8fae92f7200afeb2be503ff1a17c3b4e4ed7b76f008c2810a31"
url: "https://pub.dev"
source: hosted
version: "2.9.0"
camera_web:
dependency: transitive
description:
name: camera_web
sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f"
url: "https://pub.dev"
source: hosted
version: "0.3.5"
characters:
dependency: transitive
description:
@ -201,14 +225,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.0"
colorfilter_generator:
dependency: transitive
description:
name: colorfilter_generator
sha256: ccc2995e440b1d828d55d99150e7cad64624f3cb4a1e235000de3f93cf10d35c
url: "https://pub.dev"
source: hosted
version: "0.0.8"
connectivity_plus:
dependency: "direct main"
description:
@ -869,14 +885,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.11.1"
matrix2d:
dependency: transitive
description:
name: matrix2d
sha256: "188718dd3bc2a31e372cfd0791b0f77f4f13ea76164147342cc378d9132949e7"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
meta:
dependency: transitive
description:
@ -1173,14 +1181,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.2"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
source: hosted
version: "0.28.0"
screenshot:
dependency: "direct main"
description:

View file

@ -10,9 +10,6 @@ environment:
sdk: ^3.6.0
dependencies:
camerawesome: ^2.1.0
# camerawesome:
# path: ../CamerAwesome
collection: ^1.18.0
connectivity_plus: ^6.1.2
drift: ^2.25.1
@ -54,6 +51,7 @@ dependencies:
screenshot: ^3.0.0
url_launcher: ^6.3.1
web_socket_channel: ^3.0.1
camera: ^0.11.1
dev_dependencies:
flutter_test: