add barcode scanner and remove tutorial

This commit is contained in:
otsmr 2025-12-07 16:02:08 +01:00
parent 9667ea21b6
commit 76b617e63a
13 changed files with 73 additions and 277 deletions

0
.gitmodules vendored Normal file
View file

View file

@ -1,9 +1,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<!-- The INTERNET permission is required for development. Specifically, <!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<application android:usesCleartextTraffic="true" > <application android:usesCleartextTraffic="true" >
</application> <!-- // https://github.com/juliansteenbakker/mobile_scanner/issues/553 -->
<meta-data android:name="firebase_performance_collection_deactivated" android:value="true" />
<!-- <service
android:name="com.google.android.datatransport.runtime.scheduling.jobscheduling.JobInfoSchedulerService"
tools:node="remove">
</service> -->
</application>
</manifest> </manifest>

View file

@ -1,4 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<application <application
android:label="twonly" android:label="twonly"
android:name="${applicationName}" android:name="${applicationName}"
@ -33,6 +33,12 @@
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<!-- // https://github.com/juliansteenbakker/mobile_scanner/issues/553 -->
<meta-data android:name="firebase_performance_collection_deactivated" android:value="true" />
<service
android:name="com.google.android.datatransport.runtime.scheduling.jobscheduling.JobInfoSchedulerService"
tools:node="remove">
</service>
<!-- <service <!-- <service
android:name="com.pravera.flutter_foreground_task.service.ForegroundService" android:name="com.pravera.flutter_foreground_task.service.ForegroundService"

View file

@ -8,4 +8,5 @@
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
<application android:usesCleartextTraffic="true" > <application android:usesCleartextTraffic="true" >
</application> </application>
<meta-data android:name="firebase_performance_collection_deactivated" android:value="true" />
</manifest> </manifest>

View file

@ -85,5 +85,9 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<!--Disable Firebase Telemetry-->
<key>firebase_performance_collection_deactivated</key>
<true/>
<!--...-->
</dict> </dict>
</plist> </plist>

View file

@ -23,24 +23,26 @@ class BarcodeDetectorPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final Paint paint = Paint() final paint = Paint()
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = 3.0 ..strokeWidth = 3.0
..color = Colors.lightGreenAccent; ..color = Colors.lightGreenAccent;
final Paint background = Paint()..color = Color(0x99000000); final background = Paint()..color = const Color(0x99000000);
for (final Barcode barcode in barcodes) { for (final barcode in barcodes) {
final ParagraphBuilder builder = ParagraphBuilder( final builder = ParagraphBuilder(
ParagraphStyle( ParagraphStyle(
textAlign: TextAlign.left, textAlign: TextAlign.left,
fontSize: 16, fontSize: 16,
textDirection: TextDirection.ltr), textDirection: TextDirection.ltr,
); ),
builder.pushStyle( )
ui.TextStyle(color: Colors.lightGreenAccent, background: background)); ..pushStyle(
builder.addText('${barcode.displayValue}'); ui.TextStyle(color: Colors.lightGreenAccent, background: background),
builder.pop(); )
..addText('${barcode.displayValue}')
..pop();
final left = translateX( final left = translateX(
barcode.boundingBox.left, barcode.boundingBox.left,
@ -77,16 +79,16 @@ class BarcodeDetectorPainter extends CustomPainter {
// paint, // paint,
// ); // );
final List<Offset> cornerPoints = <Offset>[]; final cornerPoints = <Offset>[];
for (final point in barcode.cornerPoints) { for (final point in barcode.cornerPoints) {
final double x = translateX( final x = translateX(
point.x.toDouble(), point.x.toDouble(),
size, size,
imageSize, imageSize,
rotation, rotation,
cameraLensDirection, cameraLensDirection,
); );
final double y = translateY( final y = translateY(
point.y.toDouble(), point.y.toDouble(),
size, size,
imageSize, imageSize,
@ -99,19 +101,22 @@ class BarcodeDetectorPainter extends CustomPainter {
// Add the first point to close the polygon // Add the first point to close the polygon
cornerPoints.add(cornerPoints.first); cornerPoints.add(cornerPoints.first);
canvas.drawPoints(PointMode.polygon, cornerPoints, paint); canvas
..drawPoints(PointMode.polygon, cornerPoints, paint)
canvas.drawParagraph( ..drawParagraph(
builder.build() builder.build()
..layout(ParagraphConstraints( ..layout(
ParagraphConstraints(
width: (right - left).abs(), width: (right - left).abs(),
)), ),
),
Offset( Offset(
Platform.isAndroid && Platform.isAndroid &&
cameraLensDirection == CameraLensDirection.front cameraLensDirection == CameraLensDirection.front
? right ? right
: left, : left,
top), top,
),
); );
} }
} }

View file

@ -26,6 +26,7 @@ double translateX(
switch (cameraLensDirection) { switch (cameraLensDirection) {
case CameraLensDirection.back: case CameraLensDirection.back:
return x * canvasSize.width / imageSize.width; return x * canvasSize.width / imageSize.width;
// ignore: no_default_cases
default: default:
return canvasSize.width - x * canvasSize.width / imageSize.width; return canvasSize.width - x * canvasSize.width / imageSize.width;
} }

View file

@ -23,7 +23,6 @@ import 'package:twonly/src/views/settings/help/changelog.view.dart';
import 'package:twonly/src/views/settings/profile/profile.view.dart'; import 'package:twonly/src/views/settings/profile/profile.view.dart';
import 'package:twonly/src/views/settings/settings_main.view.dart'; import 'package:twonly/src/views/settings/settings_main.view.dart';
import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
import 'package:twonly/src/views/tutorial/tutorials.dart';
class ChatListView extends StatefulWidget { class ChatListView extends StatefulWidget {
const ChatListView({super.key}); const ChatListView({super.key});
@ -38,7 +37,6 @@ class _ChatListViewState extends State<ChatListView> {
List<Group> _groupsArchived = []; List<Group> _groupsArchived = [];
GlobalKey searchForOtherUsers = GlobalKey(); GlobalKey searchForOtherUsers = GlobalKey();
Timer? tutorial;
bool showFeedbackShortcut = false; bool showFeedbackShortcut = false;
@override @override
@ -58,16 +56,6 @@ class _ChatListViewState extends State<ChatListView> {
}); });
}); });
tutorial = Timer(const Duration(seconds: 1), () async {
tutorial = null;
if (!mounted) return;
await showChatListTutorialSearchOtherUsers(context, searchForOtherUsers);
if (!mounted) return;
// if (_groupsNotPinned.isNotEmpty) {
// await showChatListTutorialContextMenu(context, firstUserListItemKey);
// }
});
final changeLog = await rootBundle.loadString('CHANGELOG.md'); final changeLog = await rootBundle.loadString('CHANGELOG.md');
final changeLogHash = final changeLogHash =
(await compute(Sha256().hash, changeLog.codeUnits)).bytes; (await compute(Sha256().hash, changeLog.codeUnits)).bytes;
@ -97,7 +85,6 @@ class _ChatListViewState extends State<ChatListView> {
@override @override
void dispose() { void dispose() {
tutorial?.cancel();
_contactsSub.cancel(); _contactsSub.cancel();
super.dispose(); super.dispose();
} }

View file

@ -22,7 +22,6 @@ import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/views/contact/contact.view.dart'; import 'package:twonly/src/views/contact/contact.view.dart';
import 'package:twonly/src/views/groups/group.view.dart'; import 'package:twonly/src/views/groups/group.view.dart';
import 'package:twonly/src/views/tutorial/tutorials.dart';
Color getMessageColor(Message message) { Color getMessageColor(Message message) {
return (message.senderId == null) return (message.senderId == null)
@ -89,7 +88,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
Message? quotesMessage; Message? quotesMessage;
GlobalKey verifyShieldKey = GlobalKey(); GlobalKey verifyShieldKey = GlobalKey();
late FocusNode textFieldFocus; late FocusNode textFieldFocus;
Timer? tutorial;
final ItemScrollController itemScrollController = ItemScrollController(); final ItemScrollController itemScrollController = ItemScrollController();
int? focusedScrollItem; int? focusedScrollItem;
@ -99,12 +97,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
group = widget.group; group = widget.group;
textFieldFocus = FocusNode(); textFieldFocus = FocusNode();
initStreams(); initStreams();
tutorial = Timer(const Duration(seconds: 1), () async {
tutorial = null;
if (!mounted) return;
await showVerifyShieldTutorial(context, verifyShieldKey);
});
} }
@override @override
@ -114,7 +106,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
contactSub?.cancel(); contactSub?.cancel();
groupActionsSub?.cancel(); groupActionsSub?.cancel();
lastOpenedMessageByContactSub?.cancel(); lastOpenedMessageByContactSub?.cancel();
tutorial?.cancel();
super.dispose(); super.dispose();
} }

View file

@ -156,7 +156,7 @@ class HomeViewState extends State<HomeView> {
CustomPaint? _customPaint; CustomPaint? _customPaint;
String? _text; String? _text;
final _orientations = { final Map<DeviceOrientation, int> _orientations = {
DeviceOrientation.portraitUp: 0, DeviceOrientation.portraitUp: 0,
DeviceOrientation.landscapeLeft: 90, DeviceOrientation.landscapeLeft: 90,
DeviceOrientation.portraitDown: 180, DeviceOrientation.portraitDown: 180,

View file

@ -62,26 +62,26 @@ class _HelpViewState extends State<HelpView> {
); );
}, },
), ),
ListTile( // ListTile(
title: Text(context.lang.settingsResetTutorials), // title: Text(context.lang.settingsResetTutorials),
subtitle: Text( // subtitle: Text(
context.lang.settingsResetTutorialsDesc, // context.lang.settingsResetTutorialsDesc,
style: const TextStyle(fontSize: 12), // style: const TextStyle(fontSize: 12),
), // ),
onTap: () async { // onTap: () async {
await updateUserdata((user) { // await updateUserdata((user) {
user.tutorialDisplayed = []; // user.tutorialDisplayed = [];
return user; // return user;
}); // });
if (!context.mounted) return; // if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( // ScaffoldMessenger.of(context).showSnackBar(
SnackBar( // SnackBar(
content: Text(context.lang.settingsResetTutorialsSuccess), // content: Text(context.lang.settingsResetTutorialsSuccess),
duration: const Duration(seconds: 3), // duration: const Duration(seconds: 3),
), // ),
); // );
}, // },
), // ),
const Divider(), const Divider(),
ListTile( ListTile(
title: Text(context.lang.allowErrorTracking), title: Text(context.lang.allowErrorTracking),

View file

@ -1,114 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:tutorial_coach_mark/tutorial_coach_mark.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
Future<void> showTutorial(
BuildContext context,
List<TargetFocus> targets,
) async {
final completer = Completer<dynamic>();
TutorialCoachMark(
targets: targets,
colorShadow: context.color.primary,
textSkip: context.lang.ok,
alignSkip: Alignment.bottomCenter,
textStyleSkip: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 20,
),
onSkip: () {
completer.complete();
return true;
},
onFinish: () {
completer.complete();
},
).show(context: context);
await completer.future;
}
Future<bool> checkIfTutorialAlreadyShown(String tutorialId) async {
final user = await getUser();
if (user == null) return true;
user.tutorialDisplayed ??= [];
if (user.tutorialDisplayed!.contains(tutorialId)) {
return true;
}
user.tutorialDisplayed!.add(tutorialId);
await updateUserdata((u) {
u.tutorialDisplayed = user.tutorialDisplayed;
return u;
});
return false;
}
TargetFocus getTargetFocus(
BuildContext context,
GlobalKey key,
String title,
String body,
) {
final renderBox = key.currentContext!.findRenderObject()! as RenderBox;
final position = renderBox.localToGlobal(Offset.zero);
final screenHeight = MediaQuery.of(context).size.height;
final centerY = screenHeight / 2;
double top = 0;
double bottom = 0;
if (position.dy < centerY) {
bottom = 0;
top = position.dy;
} else {
bottom = centerY;
top = 0;
}
return TargetFocus(
identify: title,
keyTarget: key,
contents: [
TargetContent(
align: ContentAlign.custom,
customPosition: CustomTargetContentPosition(
left: 0,
right: 0,
top: top,
bottom: bottom,
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black,
fontSize: 20,
),
),
Padding(
padding: const EdgeInsets.only(top: 10),
child: Text(
body,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.black,
fontSize: 16,
),
),
),
],
),
),
],
);
}

View file

@ -1,91 +0,0 @@
import 'package:flutter/material.dart';
import 'package:mutex/mutex.dart';
import 'package:tutorial_coach_mark/tutorial_coach_mark.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/tutorial/show_tutorial.dart';
final lockDisplayTutorial = Mutex();
Future<void> showChatListTutorialSearchOtherUsers(
BuildContext context,
GlobalKey searchForOtherUsers,
) async {
await lockDisplayTutorial.protect(() async {
if (await checkIfTutorialAlreadyShown('chat_list:search_users')) {
return;
}
if (!context.mounted) return;
final targets = <TargetFocus>[
getTargetFocus(
context,
searchForOtherUsers,
context.lang.tutorialChatListSearchUsersTitle,
context.lang.tutorialChatListSearchUsersDesc,
),
];
await showTutorial(context, targets);
});
}
Future<void> showChatListTutorialContextMenu(
BuildContext context,
GlobalKey firstUserListItemKey,
) async {
await lockDisplayTutorial.protect(() async {
if (await checkIfTutorialAlreadyShown('chat_list:context_menu')) {
return;
}
if (!context.mounted) return;
final targets = <TargetFocus>[
getTargetFocus(
context,
firstUserListItemKey,
context.lang.tutorialChatListContextMenuTitle,
context.lang.tutorialChatListContextMenuDesc,
),
];
await showTutorial(context, targets);
});
}
Future<void> showVerifyShieldTutorial(
BuildContext context,
GlobalKey firstUserListItemKey,
) async {
await lockDisplayTutorial.protect(() async {
if (await checkIfTutorialAlreadyShown('chat_messages:verify_shield')) {
return;
}
if (!context.mounted) return;
final targets = <TargetFocus>[
getTargetFocus(
context,
firstUserListItemKey,
context.lang.tutorialChatMessagesVerifyShieldTitle,
context.lang.tutorialChatMessagesVerifyShieldDesc,
),
];
await showTutorial(context, targets);
});
}
Future<void> showReopenMediaFilesTutorial(
BuildContext context,
GlobalKey firstUserListItemKey,
) async {
await lockDisplayTutorial.protect(() async {
if (await checkIfTutorialAlreadyShown('chat_messages:reopen_message')) {
return;
}
if (!context.mounted) return;
final targets = <TargetFocus>[
getTargetFocus(
context,
firstUserListItemKey,
context.lang.tutorialChatMessagesReopenMessageTitle,
context.lang.tutorialChatMessagesReopenMessageDesc,
),
];
await showTutorial(context, targets);
});
}