Skip to content

Commit

Permalink
QR code scanner
Browse files Browse the repository at this point in the history
  • Loading branch information
Zverik committed May 25, 2024
1 parent 4fad866 commit 154ebce
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 21 deletions.
10 changes: 6 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

_Unreleased_

* Added the recent walked path display in small blue dots.
* Added the recent walked path display as small blue dots.
* GeoScribbles drawing is locked by default.
* QR code scanner for the website field.
* When deleting an amenity with a unique address, suggest keeping the address.
* Now possible to move nodes that are relation members.
* Fixed blue marker for unmovable objects in the editor.
* Minimum supported Android version is 5.0 now, due to Flutter upgrade.
* Fixed a blue marker for unmovable objects in the editor.
* Fixed the white map when adding an object on iOS.
* Minimal supported Android version is 5.0 now, due to the Flutter upgrade.

## 5.0

Expand All @@ -34,7 +36,7 @@ _Released on 2024-05-06_
* Speed fields are not filtered out now.
* Removed the password login button.
* Switched to Dart 3 and upgraded `flutter_map` to version 6.
* Minimum supported iOS version is 12 now.
* Minimal supported iOS version is 12 now.
* Translations into Estonian (thanks August Murasev Frokjaer), Odia (thanks Soumendra Kumar Sahoo),
and major updates to Croatian (thanks Milo Ivir).

Expand Down
1 change: 1 addition & 0 deletions android/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
dev.steenbakker.mobile_scanner.useUnbundled=true
2 changes: 2 additions & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
<string>This app is for mapping amenities around you, so it needs to know where you are.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>The editor does not use location in the background.</string>
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan QR codes.</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
Expand Down
46 changes: 46 additions & 0 deletions lib/fields/helpers/qr_code.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class QrCodeScanner extends StatefulWidget {
const QrCodeScanner({super.key});

@override
State<QrCodeScanner> createState() => _QrCodeScannerState();
}

class _QrCodeScannerState extends State<QrCodeScanner> {
bool done = false;

@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(loc.fieldWebsiteQR),
),
body: MobileScanner(
onDetect: (codes) {
if (!done && mounted && codes.barcodes.isNotEmpty) {
final code = codes.barcodes.first;
String? url;
if (code.type == BarcodeType.url) {
url = code.url?.url;
} else if (code.type == BarcodeType.text ||
code.type == BarcodeType.unknown) {
final value = code.displayValue;
if (value != null && value.startsWith("http")) {
url = value;
}
}

if (url != null) {
done = true; // we need this because it scans twice sometimes
Navigator.pop(context, url);
}
}
},
),
);
}
}
47 changes: 31 additions & 16 deletions lib/fields/helpers/website_fmt.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ abstract class WebsiteProvider {
required this.label});

/// Whether user-entered value can be parsed.
bool isValid(String full);
/// Set url: true to make sure it's the proper provider for an URL.
bool isValid(String full, {bool url = false});

/// Converts user-entered value into a proper tag value.
String format(String value);
Expand Down Expand Up @@ -89,7 +90,7 @@ class UrlWebsiteProvider extends WebsiteProvider {
static const kValidWebsiteUrlSchemes = ["http", "https"];

@override
bool isValid(String full) {
bool isValid(String full, {bool url = false}) {
try {
final uri = Uri.parse(full.replaceAll(' ', '').trim());
if (uri.hasScheme && !kValidWebsiteUrlSchemes.contains(uri.scheme)) {
Expand Down Expand Up @@ -122,6 +123,7 @@ class UrlWebsiteProvider extends WebsiteProvider {

class _ProviderHelper extends WebsiteProvider {
final RegExp _regexp;
final RegExp? _regexpUrl;
final String _format;

_ProviderHelper({
Expand All @@ -130,14 +132,17 @@ class _ProviderHelper extends WebsiteProvider {
required super.key,
required super.label,
required RegExp regexp,
RegExp? regexpUrl,
String? format,
}) : _regexp = regexp,
_regexpUrl = regexpUrl,
_format = format ?? '%s',
super(
prefixes: prefixes ?? const []);
super(prefixes: prefixes ?? const []);

@override
bool isValid(String full) => _regexp.hasMatch(full.trim());
bool isValid(String full, {bool url = false}) => !url
? _regexp.hasMatch(full.trim())
: _regexpUrl?.hasMatch(full) ?? false;

@override
String? url(String value) {
Expand Down Expand Up @@ -171,8 +176,10 @@ class FacebookProvider extends _ProviderHelper {
label: 'Facebook',
prefixes: ['fb', 'facebook', 'face'],
key: 'contact:facebook',
regexp: RegExp(r'(?:facebook(?:\.com)?/)?([^/ ]+)/?$'),
format: 'https://www.facebook.com/%s',
regexp: RegExp(
r'(?:facebook(?:\.com)?/)?((?:groups/)?[^/? ]+)/?(?:\?.*)?$'),
regexpUrl: RegExp(r'facebook\.com/((?:groups/)?[^/? ]+)/?(?:\?.*)?$'),
format: 'https://www.facebook.com/%s/',
);
}

Expand All @@ -184,6 +191,7 @@ class InstagramProvider extends _ProviderHelper {
prefixes: ['i', 'insta', 'instagram', 'инстаграм'],
key: 'contact:instagram',
regexp: RegExp(r'(?:instagram(?:\.com)?/)?([^/ ?]+)/?(\?[\w=]+)?$'),
regexpUrl: RegExp(r'instagram\.com/([^/ ?]+)/?(\?[\w=]+)?$'),
format: 'https://www.instagram.com/%s',
);
}
Expand All @@ -195,7 +203,8 @@ class VkProvider extends _ProviderHelper {
label: 'Vk',
prefixes: ['vk', 'вк'],
key: 'contact:vk',
regexp: RegExp(r'(?:vk(?:ontakte)?(?:\.com|\.ru)?/)?([^/ ]+)/?$'),
regexp: RegExp(r'(?:vk(?:ontakte)?(?:\.com|\.ru)?/)?([^/? ]+)/?$'),
regexpUrl: RegExp(r'vk(?:ontakte)?\.(?:com|ru)/([^/? ]+)/?$'),
format: 'https://vk.com/%s',
);

Expand All @@ -218,6 +227,7 @@ class TwitterProvider extends _ProviderHelper {
prefixes: ['tw', 'twitter'],
key: 'contact:twitter',
regexp: RegExp(r'(?:twitter(?:\.com)?/)?([^/ ]+)/?$'),
regexpUrl: RegExp(r'(?:twitter|//x)\.com/([^/ ]+)/?$'),
format: 'https://twitter.com/%s',
);
}
Expand All @@ -230,6 +240,7 @@ class OkProvider extends _ProviderHelper {
prefixes: ['ok', 'ок', 'однокл', 'одноклассники'],
key: 'contact:ok',
regexp: RegExp(r'(?:ok\.ru/)?([^/ ]+)/?$'),
regexpUrl: RegExp(r'ok\.ru/([^/ ]+)/?$'),
format: 'https://ok.ru/%s',
);
}
Expand All @@ -241,7 +252,8 @@ class TelegramProvider extends _ProviderHelper {
label: 'Telegram',
prefixes: ['tg', 'telegram'],
key: 'contact:telegram',
regexp: RegExp(r'(?://t.me/|^t.me/)?([^/ ]+)/?$'),
regexp: RegExp(r'(?://t.me/|^t.me/)?([^/? ]+)/?$'),
regexpUrl: RegExp(r'//t.me/([^/? ]+)/?$'),
format: 'https://t.me/%s',
);
}
Expand Down Expand Up @@ -271,19 +283,21 @@ class ViberProvider extends _ProviderHelper {
key: 'contact:viber',
regexp: RegExp(
r'(?:chats\.viber\.com/|chatURI=)?(\+[\d -]+\d|[^/ ]+)/?$'),
regexpUrl: RegExp(r'chats\.viber\.com/(\+[\d -]+\d|[^/ ]+)/?$'),
);
}

class LinkedinProvider extends _ProviderHelper {
LinkedinProvider()
: super(
icon: LineIcons.linkedin,
label: 'LinkedIn',
prefixes: ['linkedin', 'li'],
key: 'contact:linkedin',
regexp: RegExp(r'(?:linkedin\.com/company/)?([^/ ]+)/?$'),
format: 'https://www.linkedin.com/company/%s',
);
icon: LineIcons.linkedin,
label: 'LinkedIn',
prefixes: ['linkedin', 'li'],
key: 'contact:linkedin',
regexp: RegExp(r'(?:linkedin\.com/company/)?([^/? ]+)/?$'),
regexpUrl: RegExp(r'linkedin\.com/company/([^/? ]+)/?$'),
format: 'https://www.linkedin.com/company/%s',
);
}

class TikTokProvider extends _ProviderHelper {
Expand All @@ -294,6 +308,7 @@ class TikTokProvider extends _ProviderHelper {
prefixes: ['tiktok', 'tt'],
key: 'contact:tiktok',
regexp: RegExp(r'(?:tiktok\.com/)?@?([^@ /?]+)'),
regexpUrl: RegExp(r'tiktok\.com/@([^@ /?]+)'),
format: 'https://www.tiktok.com/@%s',
);
}
42 changes: 41 additions & 1 deletion lib/fields/website.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import 'package:every_door/constants.dart';
import 'package:every_door/fields/helpers/qr_code.dart';
import 'package:every_door/models/amenity.dart';
import 'package:every_door/providers/editor_settings.dart';
import 'package:flutter/material.dart';
import 'package:every_door/models/field.dart';
import 'package:logging/logging.dart';
import 'package:url_launcher/url_launcher.dart';
import 'helpers/website_fmt.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Expand Down Expand Up @@ -34,6 +36,8 @@ class WebsiteInputField extends ConsumerStatefulWidget {
}

class _WebsiteInputFieldState extends ConsumerState<WebsiteInputField> {
static final _logger = Logger('WebsiteInputField');

late TextEditingController _controller;
late WebsiteProvider _provider;
late FocusNode _fieldFocus;
Expand Down Expand Up @@ -76,6 +80,31 @@ class _WebsiteInputFieldState extends ConsumerState<WebsiteInputField> {
return true;
}

detectAndSubmitUrl(String url) {
WebsiteProvider? found;
// Skip first for the end (it's the generic URL provider).
for (int i = 1; i < websiteProviders.length; i++) {
if (websiteProviders[i].isValid(url, url: true)) {
found = websiteProviders[i];
break;
}
}
if (found == null && websiteProviders[0].isValid(url, url: true)) {
found = websiteProviders[0];
}

_logger.info('Provider ${found?.label ?? "unknown"} for $url');
if (found != null) {
setState(() {
// Weird we need to do this check twice.
if (found != null) {
found.setValue(widget.element, found.format(url),
preferContact: ref.read(editorSettingsProvider).preferContact);
}
});
}
}

showProviderChooser() async {
final result = await showModalBottomSheet(
context: context,
Expand Down Expand Up @@ -182,7 +211,18 @@ class _WebsiteInputFieldState extends ConsumerState<WebsiteInputField> {
},
),
),
)
),
IconButton(
icon: Icon(
Icons.qr_code_scanner,
size: 30.0,
),
onPressed: () async {
final String? detected = await Navigator.push(context,
MaterialPageRoute(builder: (_) => QrCodeScanner()));
if (detected != null) detectAndSubmitUrl(detected);
},
),
],
),
for (final website in websites)
Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,10 @@
"@fieldHeightAny": {
"description": "Validation message for when meters or feet are expected."
},
"fieldWebsiteQR": "Scan a QR Code",
"@fieldWebsiteQR": {
"description": "Page title for the QR code scanner."
},
"messageNoData": "No data, tap here to download",
"@messageNoData": {
"description": "Message that appears on the main screen when there's no OSM data."
Expand Down
8 changes: 8 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: b8c0e9afcfd52534f85ec666f3d52156f560b5e6c25b1e3d4fe2087763607926
url: "https://pub.dev"
source: hosted
version: "5.1.1"
node_preamble:
dependency: transitive
description:
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies:
latlong2: ^0.9.0
line_icons: ^2.0.1
logging: ^1.0.2
mobile_scanner: ^5.1.1
oauth2_client: ^3.2.2
path: ^1.8.1
path_drawing: ^1.0.0
Expand Down

0 comments on commit 154ebce

Please sign in to comment.