mirror of
https://github.com/inventree/inventree-app.git
synced 2026-08-18 17:26:28 +00:00
Add intent wedge scanner, MDM support for Android (#863)
Add Android intent wedge scanner support with auto configuration for supported Zebra and Datalogic devices. Intent wedge scans can be done without pressing the scan button first. Adds the ability for an MDM server to push settings on Android.
This commit is contained in:
@@ -94,4 +94,5 @@ dependencies {
|
||||
androidTestImplementation 'com.android.support:multidex:2.0.1'
|
||||
implementation "androidx.core:core:1.9.0"
|
||||
implementation 'androidx.appcompat:appcompat:1.6.0'
|
||||
compileOnly 'com.datalogic:datalogic-android-sdk:1.50'
|
||||
}
|
||||
|
||||
@@ -15,11 +15,22 @@
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
|
||||
<!-- For intent wedge detection -->
|
||||
<package android:name="com.symbol.datawedge" />
|
||||
<package android:name="com.datalogic.decode" />
|
||||
<package android:name="com.datalogic.decodewedge" />
|
||||
<package android:name="com.datalogic.systemsettings" />
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:label="InvenTree"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
|
||||
<uses-library
|
||||
android:name="com.datalogic.device"
|
||||
android:required="false" />
|
||||
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
@@ -1,6 +1,172 @@
|
||||
package inventree.inventree_app;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.RestrictionsManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity;
|
||||
import io.flutter.embedding.engine.FlutterEngine;
|
||||
import io.flutter.plugin.common.EventChannel;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
|
||||
static final String ACTION_CUSTOM_SCAN = "inventree.inventree_app.SCAN";
|
||||
|
||||
// Datalogic default wedge settings
|
||||
static final String ACTION_DATALOGIC_DECODE = "com.datalogic.decodewedge.decode_action";
|
||||
static final String CATEGORY_DATALOGIC_DECODE = "com.datalogic.decodewedge.decode_category";
|
||||
|
||||
private static final String EVENT_CHANNEL = "inventree/datawedge_scans";
|
||||
private static final String MANAGED_CONFIG_CHANNEL = "inventree/managed_config";
|
||||
private static final String WEDGE_CHANNEL = "inventree/wedge";
|
||||
|
||||
private EventChannel.EventSink eventSink;
|
||||
|
||||
private final BroadcastReceiver scanReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent == null) return;
|
||||
|
||||
String action = intent.getAction();
|
||||
if (action == null) return;
|
||||
|
||||
// Check if it matches one of the strings
|
||||
if (!ACTION_CUSTOM_SCAN.equals(action) && !ACTION_DATALOGIC_DECODE.equals(action)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String data = intent.getStringExtra("com.symbol.datawedge.data_string");
|
||||
if (data == null || data.isEmpty()) {
|
||||
data = intent.getStringExtra("com.datalogic.decode.intentwedge.barcode_string");
|
||||
}
|
||||
if (data == null || data.isEmpty()) return;
|
||||
if (eventSink == null) return;
|
||||
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("data", data);
|
||||
payload.put("action", action);
|
||||
eventSink.success(payload);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Auto-configure the datawedge for Zebra/Datalogic
|
||||
WedgeConfigurator.configure(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine);
|
||||
|
||||
new EventChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), EVENT_CHANNEL)
|
||||
.setStreamHandler(new EventChannel.StreamHandler() {
|
||||
@Override
|
||||
public void onListen(Object arguments, EventChannel.EventSink events) {
|
||||
eventSink = events;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(Object arguments) {
|
||||
eventSink = null;
|
||||
}
|
||||
});
|
||||
|
||||
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), MANAGED_CONFIG_CHANNEL)
|
||||
.setMethodCallHandler((call, result) -> {
|
||||
if ("getManagedConfiguration".equals(call.method)) {
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
|
||||
RestrictionsManager rm =
|
||||
(RestrictionsManager) getSystemService(Context.RESTRICTIONS_SERVICE);
|
||||
|
||||
if (rm != null) {
|
||||
Bundle restrictions = rm.getApplicationRestrictions();
|
||||
|
||||
if (restrictions != null) {
|
||||
for (String key : restrictions.keySet()) {
|
||||
@SuppressWarnings("deprecation")
|
||||
Object value = restrictions.get(key);
|
||||
|
||||
if (value instanceof Boolean
|
||||
|| value instanceof Integer
|
||||
|| value instanceof Long
|
||||
|| value instanceof Double
|
||||
|| value instanceof String) {
|
||||
config.put(key, value);
|
||||
} else if (value instanceof String[]) {
|
||||
config.put(key, java.util.Arrays.asList((String[]) value));
|
||||
} else if (value != null) {
|
||||
config.put(key, value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.success(config);
|
||||
} else {
|
||||
result.notImplemented();
|
||||
}
|
||||
});
|
||||
|
||||
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), WEDGE_CHANNEL)
|
||||
.setMethodCallHandler((call, result) -> {
|
||||
if ("detectWedge".equals(call.method)) {
|
||||
String vendor = WedgeConfigurator.detectVendor(this);
|
||||
|
||||
Map<String, Object> info = new HashMap<>();
|
||||
info.put("vendor", vendor);
|
||||
info.put("supported", !WedgeConfigurator.VENDOR_NONE.equals(vendor));
|
||||
|
||||
result.success(info);
|
||||
} else {
|
||||
result.notImplemented();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanUpFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
||||
eventSink = null;
|
||||
super.cleanUpFlutterEngine(flutterEngine);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_CUSTOM_SCAN);
|
||||
filter.addAction(ACTION_DATALOGIC_DECODE);
|
||||
filter.addCategory(CATEGORY_DATALOGIC_DECODE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
registerReceiver(scanReceiver, filter, Context.RECEIVER_EXPORTED);
|
||||
} else {
|
||||
registerReceiver(scanReceiver, filter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
try {
|
||||
unregisterReceiver(scanReceiver);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// Receiver was never registered
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
package inventree.inventree_app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import com.datalogic.decode.BarcodeManager;
|
||||
import com.datalogic.decode.configuration.IntentDeliveryMode;
|
||||
import com.datalogic.decode.configuration.ScannerProperties;
|
||||
import com.datalogic.device.ErrorManager;
|
||||
import com.datalogic.device.configuration.ConfigException;
|
||||
|
||||
public class WedgeConfigurator {
|
||||
|
||||
// Zebra specific settings
|
||||
private static final String DATAWEDGE_PROFILE = "InvenTree";
|
||||
public static final String ZEBRA_BROADCAST_INTENT = "2";
|
||||
private static final String DATAWEDGE_API_ACTION = "com.symbol.datawedge.api.ACTION";
|
||||
private static final String DATAWEDGE_SET_CONFIG = "com.symbol.datawedge.api.SET_CONFIG";
|
||||
|
||||
// Datalogic retry settings
|
||||
private static final int DATALOGIC_MAX_ATTEMPTS = 5;
|
||||
private static final long DATALOGIC_RETRY_DELAY_MS = 2000;
|
||||
|
||||
// Vendor identifiers
|
||||
public static final String VENDOR_ZEBRA = "zebra";
|
||||
public static final String VENDOR_DATALOGIC = "datalogic";
|
||||
public static final String VENDOR_NONE = "none";
|
||||
|
||||
private static final Map<String, String[]> VENDOR_PACKAGES = buildVendorPackages();
|
||||
|
||||
// Spawn a new thread for Datalogic calls
|
||||
private static final ExecutorService DATALOGIC_EXECUTOR = Executors.newSingleThreadExecutor();
|
||||
|
||||
private static Map<String, String[]> buildVendorPackages() {
|
||||
Map<String, String[]> map = new LinkedHashMap<>();
|
||||
|
||||
map.put(VENDOR_ZEBRA, new String[] {"com.symbol.datawedge"});
|
||||
|
||||
map.put(
|
||||
VENDOR_DATALOGIC,
|
||||
new String[] {
|
||||
"com.datalogic.decode",
|
||||
"com.datalogic.decodewedge",
|
||||
"com.datalogic.systemsettings"
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public static String detectVendor(Context context) {
|
||||
try {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
|
||||
if (pm == null) {
|
||||
return VENDOR_NONE;
|
||||
}
|
||||
|
||||
for (Map.Entry<String, String[]> entry : VENDOR_PACKAGES.entrySet()) {
|
||||
for (String packageName : entry.getValue()) {
|
||||
if (isPackageInstalled(pm, packageName)) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
// PackageManager lookup failed
|
||||
}
|
||||
|
||||
return detectVendorByManufacturer();
|
||||
}
|
||||
|
||||
private static String detectVendorByManufacturer() {
|
||||
String manufacturer = Build.MANUFACTURER == null ? "" : Build.MANUFACTURER.toLowerCase();
|
||||
|
||||
if (manufacturer.contains("zebra") || manufacturer.contains("symbol")) {
|
||||
return VENDOR_ZEBRA;
|
||||
}
|
||||
|
||||
if (manufacturer.contains("datalogic")) {
|
||||
return VENDOR_DATALOGIC;
|
||||
}
|
||||
|
||||
return VENDOR_NONE;
|
||||
}
|
||||
|
||||
private static boolean isPackageInstalled(PackageManager pm, String packageName) {
|
||||
try {
|
||||
pm.getPackageInfo(packageName, 0);
|
||||
return true;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return false;
|
||||
} catch (Throwable e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void configure(Context context) {
|
||||
try {
|
||||
String vendor = detectVendor(context);
|
||||
|
||||
if (VENDOR_ZEBRA.equals(vendor)) {
|
||||
configureZebra(context);
|
||||
} else if (VENDOR_DATALOGIC.equals(vendor)) {
|
||||
scheduleDatalogicConfig(1);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
// Vendor detection was unsuccessful
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the Datalogic wedge configuration.
|
||||
* @param attempt Current attempt number
|
||||
*/
|
||||
private static void scheduleDatalogicConfig(int attempt) {
|
||||
new Handler(Looper.getMainLooper())
|
||||
.postDelayed(
|
||||
() -> DATALOGIC_EXECUTOR.execute(() -> {
|
||||
try {
|
||||
configureDatalogic();
|
||||
} catch (Throwable e) {
|
||||
if (attempt < DATALOGIC_MAX_ATTEMPTS) {
|
||||
scheduleDatalogicConfig(attempt + 1);
|
||||
}
|
||||
}
|
||||
}),
|
||||
DATALOGIC_RETRY_DELAY_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Zebra datawedge by creating a custom profile for the app.
|
||||
* @param context MainActivity instance passed in as context
|
||||
*/
|
||||
private static void configureZebra(Context context) {
|
||||
Bundle profileConfig = new Bundle();
|
||||
profileConfig.putString("PROFILE_NAME", DATAWEDGE_PROFILE);
|
||||
profileConfig.putString("PROFILE_ENABLED", "true");
|
||||
profileConfig.putString("CONFIG_MODE", "CREATE_IF_NOT_EXIST");
|
||||
|
||||
Bundle appConfig = new Bundle();
|
||||
appConfig.putString("PACKAGE_NAME", context.getPackageName());
|
||||
appConfig.putStringArray("ACTIVITY_LIST", new String[] {"*"});
|
||||
profileConfig.putParcelableArray("APP_LIST", new Bundle[] {appConfig});
|
||||
|
||||
ArrayList<Bundle> plugins = new ArrayList<>();
|
||||
|
||||
Bundle barcodePlugin = new Bundle();
|
||||
barcodePlugin.putString("PLUGIN_NAME", "BARCODE");
|
||||
barcodePlugin.putString("RESET_CONFIG", "true");
|
||||
|
||||
Bundle barcodeProps = new Bundle();
|
||||
barcodeProps.putString("scanner_input_enabled", "true");
|
||||
barcodeProps.putString("scanner_selection", "auto");
|
||||
barcodePlugin.putBundle("PARAM_LIST", barcodeProps);
|
||||
plugins.add(barcodePlugin);
|
||||
|
||||
Bundle intentPlugin = new Bundle();
|
||||
intentPlugin.putString("PLUGIN_NAME", "INTENT");
|
||||
intentPlugin.putString("RESET_CONFIG", "true");
|
||||
|
||||
Bundle intentProps = new Bundle();
|
||||
intentProps.putString("intent_output_enabled", "true");
|
||||
intentProps.putString("intent_action", MainActivity.ACTION_CUSTOM_SCAN);
|
||||
intentProps.putString("intent_delivery", ZEBRA_BROADCAST_INTENT);
|
||||
intentPlugin.putBundle("PARAM_LIST", intentProps);
|
||||
plugins.add(intentPlugin);
|
||||
|
||||
Bundle keystrokePlugin = new Bundle();
|
||||
keystrokePlugin.putString("PLUGIN_NAME", "KEYSTROKE");
|
||||
keystrokePlugin.putString("RESET_CONFIG", "true");
|
||||
|
||||
Bundle keystrokeProps = new Bundle();
|
||||
keystrokeProps.putString("keystroke_output_enabled", "false");
|
||||
keystrokePlugin.putBundle("PARAM_LIST", keystrokeProps);
|
||||
plugins.add(keystrokePlugin);
|
||||
|
||||
profileConfig.putParcelableArrayList("PLUGIN_CONFIG", plugins);
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(DATAWEDGE_API_ACTION);
|
||||
intent.putExtra(DATAWEDGE_SET_CONFIG, profileConfig);
|
||||
context.sendBroadcast(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to apply Datalogic wedge configuration.
|
||||
*/
|
||||
private static void configureDatalogic() {
|
||||
ErrorManager.enableExceptions(false);
|
||||
|
||||
BarcodeManager manager = new BarcodeManager();
|
||||
|
||||
ScannerProperties configuration = ScannerProperties.edit(manager);
|
||||
|
||||
if (configuration == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
// Keyboard wedge only on focus of a text field
|
||||
configuration.keyboardWedge.onlyOnFocus.set(true);
|
||||
configuration.intentWedge.enable.set(true);
|
||||
// Default action, category
|
||||
configuration.intentWedge.action.set(MainActivity.ACTION_DATALOGIC_DECODE);
|
||||
configuration.intentWedge.category.set(MainActivity.CATEGORY_DATALOGIC_DECODE);
|
||||
// Enable broadcast
|
||||
configuration.intentWedge.deliveryMode.set(IntentDeliveryMode.BROADCAST);
|
||||
|
||||
int result = configuration.store(manager, true);
|
||||
|
||||
if (result != ConfigException.SUCCESS) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import "package:inventree/api.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
import "package:inventree/barcode/camera_controller.dart";
|
||||
import "package:inventree/barcode/intent_controller.dart";
|
||||
import "package:inventree/barcode/wedge_controller.dart";
|
||||
import "package:inventree/barcode/controller.dart";
|
||||
import "package:inventree/barcode/handler.dart";
|
||||
@@ -68,6 +69,53 @@ Future<void> barcodeFailure(String msg, dynamic extra) async {
|
||||
);
|
||||
}
|
||||
|
||||
void initGlobalIntentListener() {
|
||||
bool _processing = false;
|
||||
|
||||
datawedgeStream.listen((event) async {
|
||||
if (intentScannerActive) return;
|
||||
if (_processing) return;
|
||||
|
||||
_processing = true;
|
||||
|
||||
try {
|
||||
final int controllerType =
|
||||
await InvenTreeSettingsManager().getValue(
|
||||
INV_BARCODE_SCAN_TYPE,
|
||||
BARCODE_CONTROLLER_CAMERA,
|
||||
)
|
||||
as int;
|
||||
if (controllerType != BARCODE_CONTROLLER_INTENT) return;
|
||||
|
||||
if (!InvenTreeAPI().isConnected()) return;
|
||||
|
||||
String barcode = "";
|
||||
if (event is Map) {
|
||||
final map = Map<String, dynamic>.from(event);
|
||||
barcode = (map["data"] ?? "").toString();
|
||||
} else if (event is String) {
|
||||
barcode = event;
|
||||
}
|
||||
|
||||
if (barcode.isEmpty) return;
|
||||
|
||||
if (!OneContext.hasContext) return;
|
||||
|
||||
await OneContext().navigator.push(
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (context, _, _) => IntentBarcodeController(
|
||||
BarcodeScanHandler(),
|
||||
initialBarcode: barcode,
|
||||
),
|
||||
opaque: false,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
_processing = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Launch a barcode scanner with a particular context and handler.
|
||||
*
|
||||
@@ -93,6 +141,8 @@ Future<Object?> scanBarcode(
|
||||
as int;
|
||||
|
||||
switch (barcodeControllerType) {
|
||||
case BARCODE_CONTROLLER_INTENT:
|
||||
controller = IntentBarcodeController(handler);
|
||||
case BARCODE_CONTROLLER_WEDGE:
|
||||
controller = WedgeBarcodeController(handler);
|
||||
case BARCODE_CONTROLLER_CAMERA:
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/barcode/controller.dart";
|
||||
import "package:inventree/barcode/handler.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
|
||||
bool intentScannerActive = false;
|
||||
|
||||
final Stream<dynamic> datawedgeStream = const EventChannel(
|
||||
"inventree/datawedge_scans",
|
||||
).receiveBroadcastStream();
|
||||
|
||||
class IntentBarcodeController extends InvenTreeBarcodeController {
|
||||
const IntentBarcodeController(
|
||||
BarcodeHandler handler, {
|
||||
super.key,
|
||||
this.initialBarcode,
|
||||
}) : super(handler);
|
||||
|
||||
final String? initialBarcode;
|
||||
|
||||
bool get isBackgroundScan => (initialBarcode ?? "").isNotEmpty;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _IntentBarcodeControllerState();
|
||||
}
|
||||
|
||||
class _IntentBarcodeControllerState extends InvenTreeBarcodeControllerState {
|
||||
StreamSubscription<dynamic>? _sub;
|
||||
|
||||
bool canScan = true;
|
||||
bool get scanning => mounted && canScan;
|
||||
|
||||
bool get isBackgroundScan =>
|
||||
(widget as IntentBarcodeController).isBackgroundScan;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
intentScannerActive = true;
|
||||
|
||||
if (isBackgroundScan) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
handleBackgroundScan(
|
||||
(widget as IntentBarcodeController).initialBarcode ?? "",
|
||||
);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_sub = datawedgeStream.listen((event) {
|
||||
if (!scanning) return;
|
||||
|
||||
if (event is Map) {
|
||||
final map = Map<String, dynamic>.from(event);
|
||||
final data = (map["data"] ?? "").toString();
|
||||
if (data.isNotEmpty) {
|
||||
handleBarcodeData(data);
|
||||
}
|
||||
} else if (event is String && event.isNotEmpty) {
|
||||
handleBarcodeData(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> handleBackgroundScan(String barcode) async {
|
||||
if (!mounted || barcode.isEmpty) return;
|
||||
|
||||
final NavigatorState navigator = Navigator.of(context);
|
||||
final ModalRoute<dynamic>? route = ModalRoute.of(context);
|
||||
|
||||
showLoadingOverlay();
|
||||
|
||||
try {
|
||||
await widget.handler.processBarcode(barcode);
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
|
||||
if (route != null && route.isActive) {
|
||||
navigator.removeRoute(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
intentScannerActive = false;
|
||||
_sub?.cancel();
|
||||
_sub = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pauseScan() async {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
canScan = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> resumeScan() async {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
canScan = true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isBackgroundScan) {
|
||||
return const AbsorbPointer(child: SizedBox.expand());
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(L10().scanBarcode)),
|
||||
backgroundColor: Colors.black.withValues(alpha: 0.9),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Spacer(flex: 5),
|
||||
const Icon(TablerIcons.barcode, size: 64),
|
||||
const Spacer(flex: 5),
|
||||
SizedBox(
|
||||
width: 64,
|
||||
height: 64,
|
||||
child: CircularProgressIndicator(
|
||||
color: scanning ? COLOR_ACTION : COLOR_PROGRESS,
|
||||
),
|
||||
),
|
||||
const Spacer(flex: 5),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Text(
|
||||
widget.handler.getOverlayText(context),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import "dart:io";
|
||||
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
|
||||
const MethodChannel _channel = MethodChannel("inventree/wedge");
|
||||
|
||||
class WedgeDetectionResult {
|
||||
const WedgeDetectionResult({this.vendor = "none", this.supported = false});
|
||||
|
||||
final String vendor;
|
||||
|
||||
final bool supported;
|
||||
}
|
||||
|
||||
Future<WedgeDetectionResult> detectWedgeScanner() async {
|
||||
if (kIsWeb || !Platform.isAndroid) {
|
||||
return const WedgeDetectionResult();
|
||||
}
|
||||
|
||||
try {
|
||||
final Map<String, dynamic>? result = await _channel
|
||||
.invokeMapMethod<String, dynamic>("detectWedge");
|
||||
|
||||
if (result == null) return const WedgeDetectionResult();
|
||||
|
||||
return WedgeDetectionResult(
|
||||
vendor: (result["vendor"] ?? "none").toString(),
|
||||
supported: result["supported"] == true,
|
||||
);
|
||||
} catch (error) {
|
||||
return const WedgeDetectionResult();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> applyWedgeDetection(WedgeDetectionResult result) async {
|
||||
if (result.supported) {
|
||||
await InvenTreeSettingsManager().setValue(
|
||||
INV_BARCODE_SCAN_TYPE,
|
||||
BARCODE_CONTROLLER_INTENT,
|
||||
);
|
||||
}
|
||||
|
||||
await InvenTreeSettingsManager().setValue(INV_BARCODE_WEDGE_DETECTED, true);
|
||||
}
|
||||
|
||||
Future<void> initWedgeScannerDefault() async {
|
||||
if (await InvenTreeSettingsManager().getBool(
|
||||
INV_BARCODE_WEDGE_DETECTED,
|
||||
false,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final dynamic existing = await InvenTreeSettingsManager().getValue(
|
||||
INV_BARCODE_SCAN_TYPE,
|
||||
null,
|
||||
);
|
||||
|
||||
if (existing != null) {
|
||||
await InvenTreeSettingsManager().setValue(INV_BARCODE_WEDGE_DETECTED, true);
|
||||
return;
|
||||
}
|
||||
|
||||
await applyWedgeDetection(await detectWedgeScanner());
|
||||
}
|
||||
@@ -1440,6 +1440,12 @@
|
||||
"scannerExternalDetail": "Use external scanner to read barcodes (wedge mode)",
|
||||
"@scannerExternalDetail": {},
|
||||
|
||||
"scannerIntent": "Intent Wedge",
|
||||
"@scannerIntent": {},
|
||||
|
||||
"scannerIntentDetail": "Receive scans via Android intents",
|
||||
"@scannerIntentDetail": {},
|
||||
|
||||
"scanReceivedParts": "Scan Received Parts",
|
||||
"@scanReceivedParts": {},
|
||||
|
||||
|
||||
+41
-1
@@ -1,16 +1,21 @@
|
||||
import "dart:async";
|
||||
import "dart:io";
|
||||
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
|
||||
import "package:adaptive_theme/adaptive_theme.dart";
|
||||
import "package:flutter_localizations/flutter_localizations.dart";
|
||||
import "package:flutter_localized_locales/flutter_localized_locales.dart";
|
||||
import "package:inventree/barcode/barcode.dart";
|
||||
import "package:inventree/barcode/wedge_detect.dart";
|
||||
import "package:one_context/one_context.dart";
|
||||
import "package:package_info_plus/package_info_plus.dart";
|
||||
import "package:sentry_flutter/sentry_flutter.dart";
|
||||
import "package:inventree/dsn.dart";
|
||||
|
||||
import "package:inventree/managed_config.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
import "package:inventree/inventree/sentry.dart";
|
||||
import "package:inventree/l10n/supported_locales.dart";
|
||||
@@ -22,7 +27,7 @@ import "package:inventree/widget/home.dart";
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final savedThemeMode = await AdaptiveTheme.getThemeMode();
|
||||
AdaptiveThemeMode? savedThemeMode = await AdaptiveTheme.getThemeMode();
|
||||
|
||||
await runZonedGuarded<Future<void>>(
|
||||
() async {
|
||||
@@ -58,6 +63,14 @@ Future<void> main() async {
|
||||
);
|
||||
};
|
||||
|
||||
// Get any config provided by the MDM server
|
||||
await initManagedConfiguration();
|
||||
|
||||
final AdaptiveThemeMode? managedThemeMode =
|
||||
await getPendingManagedThemeMode();
|
||||
|
||||
if (managedThemeMode != null) savedThemeMode = managedThemeMode;
|
||||
|
||||
final int orientation =
|
||||
await InvenTreeSettingsManager().getValue(
|
||||
INV_SCREEN_ORIENTATION,
|
||||
@@ -116,6 +129,27 @@ class InvenTreeAppState extends State<StatefulWidget> {
|
||||
|
||||
// Run some async init tasks
|
||||
runInitTasks();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
applyPendingThemeMode();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> applyPendingThemeMode() async {
|
||||
final AdaptiveThemeMode? mode = await getPendingManagedThemeMode();
|
||||
|
||||
if (mode == null) return;
|
||||
|
||||
final BuildContext? ctx = OneContext().context;
|
||||
|
||||
if (ctx == null) return;
|
||||
|
||||
final manager = AdaptiveTheme.maybeOf(ctx);
|
||||
|
||||
if (manager == null) return;
|
||||
|
||||
manager.setThemeMode(mode);
|
||||
await clearPendingManagedThemeMode();
|
||||
}
|
||||
|
||||
// Run app init routines in the background
|
||||
@@ -124,6 +158,12 @@ class InvenTreeAppState extends State<StatefulWidget> {
|
||||
Locale? locale = await InvenTreeSettingsManager().getSelectedLocale();
|
||||
setLocale(locale);
|
||||
|
||||
await initWedgeScannerDefault();
|
||||
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
initGlobalIntentListener();
|
||||
}
|
||||
|
||||
// First-run only: seed a demo server profile if none are configured
|
||||
await UserProfileDBManager().seedDemoProfileIfNeeded();
|
||||
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
import "dart:io";
|
||||
import "dart:ui";
|
||||
|
||||
import "package:adaptive_theme/adaptive_theme.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:inventree/l10n/supported_locales.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
import "package:inventree/user_profile.dart";
|
||||
|
||||
const String MANAGED_CONFIG_SERVER_KEY = "server";
|
||||
const String MANAGED_CONFIG_NAME_KEY = "name";
|
||||
const String MANAGED_CONFIG_TOKEN_KEY = "token";
|
||||
|
||||
const String MANAGED_CONFIG_THEME_KEY = "themeMode";
|
||||
const String MANAGED_CONFIG_LANGUAGE_KEY = "language";
|
||||
|
||||
const String _MANAGED_CONFIG_APPLIED_KEY = "managedConfigApplied";
|
||||
const String _MANAGED_SETTINGS_APPLIED_KEY = "managedSettingsApplied";
|
||||
|
||||
const String INV_MANAGED_THEME_MODE = "managedThemeMode";
|
||||
|
||||
const MethodChannel _channel = MethodChannel("inventree/managed_config");
|
||||
|
||||
enum ManagedSettingType { boolean, integer, choice }
|
||||
|
||||
class ManagedSetting {
|
||||
const ManagedSetting(
|
||||
this.key,
|
||||
this.type, {
|
||||
this.choices = const {},
|
||||
this.minimum,
|
||||
this.maximum,
|
||||
});
|
||||
|
||||
final String key;
|
||||
|
||||
final ManagedSettingType type;
|
||||
|
||||
final Map<String, int> choices;
|
||||
|
||||
final int? minimum;
|
||||
final int? maximum;
|
||||
|
||||
dynamic parse(dynamic raw) {
|
||||
if (raw == null) return null;
|
||||
|
||||
switch (type) {
|
||||
case ManagedSettingType.boolean:
|
||||
return _parseBool(raw);
|
||||
case ManagedSettingType.integer:
|
||||
return _parseInt(raw);
|
||||
case ManagedSettingType.choice:
|
||||
return _parseChoice(raw);
|
||||
}
|
||||
}
|
||||
|
||||
bool? _parseBool(dynamic raw) {
|
||||
if (raw is bool) return raw;
|
||||
|
||||
switch (raw.toString().trim().toLowerCase()) {
|
||||
case "true":
|
||||
case "1":
|
||||
case "yes":
|
||||
case "on":
|
||||
return true;
|
||||
case "false":
|
||||
case "0":
|
||||
case "no":
|
||||
case "off":
|
||||
return false;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
int? _parseInt(dynamic raw) {
|
||||
int? value;
|
||||
|
||||
if (raw is int) {
|
||||
value = raw;
|
||||
} else if (raw is num) {
|
||||
value = raw.toInt();
|
||||
} else {
|
||||
value = int.tryParse(raw.toString().trim());
|
||||
}
|
||||
|
||||
if (value == null) return null;
|
||||
|
||||
final int? min = minimum;
|
||||
final int? max = maximum;
|
||||
|
||||
if (min != null && value < min) value = min;
|
||||
if (max != null && value > max) value = max;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
int? _parseChoice(dynamic raw) {
|
||||
final String name = raw.toString().trim().toLowerCase();
|
||||
|
||||
for (final entry in choices.entries) {
|
||||
if (entry.key.toLowerCase() == name) return entry.value;
|
||||
}
|
||||
|
||||
final int? value = raw is int ? raw : int.tryParse(name);
|
||||
|
||||
if (value != null && choices.containsValue(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const List<ManagedSetting> MANAGED_SETTINGS = [
|
||||
// App settings
|
||||
ManagedSetting(
|
||||
INV_SCREEN_ORIENTATION,
|
||||
ManagedSettingType.choice,
|
||||
choices: {
|
||||
"system": SCREEN_ORIENTATION_SYSTEM,
|
||||
"portrait": SCREEN_ORIENTATION_PORTRAIT,
|
||||
"landscape": SCREEN_ORIENTATION_LANDSCAPE,
|
||||
},
|
||||
),
|
||||
ManagedSetting(INV_ENABLE_LABEL_PRINTING, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_STRICT_HTTPS, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_REPORT_ERRORS, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_SOUNDS_BARCODE, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_SOUNDS_SERVER, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_SHOW_PK, ManagedSettingType.boolean),
|
||||
|
||||
// Home Screen settings
|
||||
ManagedSetting(INV_HOME_SHOW_SUBSCRIBED, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_HOME_SHOW_PO, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_HOME_SHOW_SO, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_HOME_SHOW_SHIPMENTS, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_HOME_SHOW_BUILD, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_HOME_SHOW_MANUFACTURERS, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_HOME_SHOW_CUSTOMERS, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_HOME_SHOW_SUPPLIERS, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_HOME_SHOW_TRANSFER, ManagedSettingType.boolean),
|
||||
|
||||
// Barcode settings
|
||||
ManagedSetting(
|
||||
INV_BARCODE_SCAN_TYPE,
|
||||
ManagedSettingType.choice,
|
||||
choices: {
|
||||
"camera": BARCODE_CONTROLLER_CAMERA,
|
||||
"wedge": BARCODE_CONTROLLER_WEDGE,
|
||||
"intent": BARCODE_CONTROLLER_INTENT,
|
||||
},
|
||||
),
|
||||
ManagedSetting(
|
||||
INV_BARCODE_SCAN_DELAY,
|
||||
ManagedSettingType.integer,
|
||||
minimum: 100,
|
||||
maximum: 2500,
|
||||
),
|
||||
ManagedSetting(INV_BARCODE_SCAN_SINGLE, ManagedSettingType.boolean),
|
||||
|
||||
// Part settings
|
||||
ManagedSetting(INV_PART_SHOW_BOM, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_PART_SHOW_PRICING, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_PART_SHOW_REQUIREMENTS, ManagedSettingType.boolean),
|
||||
|
||||
// Stock settings
|
||||
ManagedSetting(INV_STOCK_SHOW_HISTORY, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_STOCK_SHOW_TESTS, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_STOCK_CONFIRM_SCAN, ManagedSettingType.boolean),
|
||||
|
||||
// Purchase Order settings
|
||||
ManagedSetting(INV_PO_ENABLE, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_PO_SHOW_CAMERA, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_PO_CONFIRM_SCAN, ManagedSettingType.boolean),
|
||||
|
||||
// Sales Order settings
|
||||
ManagedSetting(INV_SO_ENABLE, ManagedSettingType.boolean),
|
||||
ManagedSetting(INV_SO_SHOW_CAMERA, ManagedSettingType.boolean),
|
||||
];
|
||||
|
||||
const Map<String, AdaptiveThemeMode> MANAGED_THEME_MODES = {
|
||||
"system": AdaptiveThemeMode.system,
|
||||
"light": AdaptiveThemeMode.light,
|
||||
"dark": AdaptiveThemeMode.dark,
|
||||
};
|
||||
|
||||
Future<void> initManagedConfiguration() async {
|
||||
if (kIsWeb || !Platform.isAndroid) return;
|
||||
|
||||
Map<String, dynamic>? config;
|
||||
|
||||
try {
|
||||
config = await _channel.invokeMapMethod<String, dynamic>(
|
||||
"getManagedConfiguration",
|
||||
);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
await applyManagedConfiguration(config);
|
||||
}
|
||||
|
||||
Future<void> applyManagedConfiguration(Map<String, dynamic>? config) async {
|
||||
if (config == null || config.isEmpty) return;
|
||||
|
||||
await _applyManagedProfile(config);
|
||||
await _applyManagedSettings(config);
|
||||
}
|
||||
|
||||
Future<void> _applyManagedProfile(Map<String, dynamic> config) async {
|
||||
final String server = (config[MANAGED_CONFIG_SERVER_KEY] ?? "")
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
// Server address is required
|
||||
if (server.isEmpty) return;
|
||||
|
||||
String name = (config[MANAGED_CONFIG_NAME_KEY] ?? "").toString().trim();
|
||||
|
||||
// Profile name is required
|
||||
if (name.isEmpty) return;
|
||||
|
||||
final String token = (config[MANAGED_CONFIG_TOKEN_KEY] ?? "")
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
final String fingerprint = "${name}|${server}";
|
||||
|
||||
final String applied =
|
||||
await InvenTreeSettingsManager().getValue(_MANAGED_CONFIG_APPLIED_KEY, "")
|
||||
as String;
|
||||
|
||||
final manager = UserProfileDBManager();
|
||||
|
||||
UserProfile? profile = await manager.getProfileByName(name);
|
||||
|
||||
// Compare token against stored token
|
||||
final bool tokenChanged = token.isNotEmpty && profile?.token != token;
|
||||
|
||||
// Check if anything actually changed
|
||||
if (applied == fingerprint && !tokenChanged) return;
|
||||
|
||||
// No demo profile
|
||||
await InvenTreeSettingsManager().setValue("demo_profile_added", true);
|
||||
|
||||
if (profile == null) {
|
||||
profile = UserProfile(name: name, server: server, token: token);
|
||||
|
||||
await manager.addProfile(profile);
|
||||
} else {
|
||||
profile.server = server;
|
||||
|
||||
if (token.isNotEmpty) profile.token = token;
|
||||
|
||||
await manager.updateProfile(profile);
|
||||
}
|
||||
|
||||
// Select the profile
|
||||
await manager.selectProfileByName(name);
|
||||
|
||||
await InvenTreeSettingsManager().setValue(
|
||||
_MANAGED_CONFIG_APPLIED_KEY,
|
||||
fingerprint,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _applyManagedSettings(Map<String, dynamic> config) async {
|
||||
final Map<String, dynamic> values = {};
|
||||
|
||||
for (final setting in MANAGED_SETTINGS) {
|
||||
if (!config.containsKey(setting.key)) continue;
|
||||
|
||||
final dynamic value = setting.parse(config[setting.key]);
|
||||
|
||||
if (value == null) continue;
|
||||
|
||||
values[setting.key] = value;
|
||||
}
|
||||
|
||||
AdaptiveThemeMode? themeMode;
|
||||
|
||||
if (config.containsKey(MANAGED_CONFIG_THEME_KEY)) {
|
||||
final String mode = config[MANAGED_CONFIG_THEME_KEY]
|
||||
.toString()
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
themeMode = MANAGED_THEME_MODES[mode];
|
||||
}
|
||||
|
||||
bool applyLocale = false;
|
||||
Locale? locale;
|
||||
|
||||
if (config.containsKey(MANAGED_CONFIG_LANGUAGE_KEY)) {
|
||||
final String language = config[MANAGED_CONFIG_LANGUAGE_KEY]
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
if (language.isEmpty) {
|
||||
applyLocale = true;
|
||||
} else {
|
||||
locale = matchSupportedLocale(language);
|
||||
|
||||
if (locale != null) applyLocale = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (values.isEmpty && themeMode == null && !applyLocale) return;
|
||||
|
||||
final List<String> elements = values.keys.toList()..sort();
|
||||
|
||||
final String fingerprint = [
|
||||
...elements.map((key) => "${key}=${values[key]}"),
|
||||
if (themeMode != null) "${MANAGED_CONFIG_THEME_KEY}=${themeMode.name}",
|
||||
if (applyLocale)
|
||||
"${MANAGED_CONFIG_LANGUAGE_KEY}=${locale?.toString() ?? ""}",
|
||||
].join("|");
|
||||
|
||||
final String applied =
|
||||
await InvenTreeSettingsManager().getValue(
|
||||
_MANAGED_SETTINGS_APPLIED_KEY,
|
||||
"",
|
||||
)
|
||||
as String;
|
||||
|
||||
if (applied == fingerprint) return;
|
||||
|
||||
for (final entry in values.entries) {
|
||||
await InvenTreeSettingsManager().setValue(entry.key, entry.value);
|
||||
}
|
||||
|
||||
if (themeMode != null) {
|
||||
await InvenTreeSettingsManager().setValue(
|
||||
INV_MANAGED_THEME_MODE,
|
||||
themeMode.name,
|
||||
);
|
||||
}
|
||||
|
||||
if (applyLocale) {
|
||||
await InvenTreeSettingsManager().setSelectedLocale(locale);
|
||||
}
|
||||
|
||||
await InvenTreeSettingsManager().setValue(
|
||||
_MANAGED_SETTINGS_APPLIED_KEY,
|
||||
fingerprint,
|
||||
);
|
||||
}
|
||||
|
||||
Locale? matchSupportedLocale(String name) {
|
||||
final String value = name.trim().replaceAll("-", "_");
|
||||
|
||||
if (value.isEmpty) return null;
|
||||
|
||||
for (final locale in supported_locales) {
|
||||
if (locale.toString().toLowerCase() == value.toLowerCase()) return locale;
|
||||
}
|
||||
|
||||
for (final locale in supported_locales) {
|
||||
if (locale.languageCode.toLowerCase() == value.toLowerCase()) return locale;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<AdaptiveThemeMode?> getPendingManagedThemeMode() async {
|
||||
final String mode =
|
||||
await InvenTreeSettingsManager().getValue(INV_MANAGED_THEME_MODE, "")
|
||||
as String;
|
||||
|
||||
if (mode.isEmpty) return null;
|
||||
|
||||
return MANAGED_THEME_MODES[mode.toLowerCase()];
|
||||
}
|
||||
|
||||
Future<void> clearPendingManagedThemeMode() async {
|
||||
await InvenTreeSettingsManager().setValue(INV_MANAGED_THEME_MODE, "");
|
||||
}
|
||||
@@ -64,6 +64,10 @@ const String INV_BARCODE_SCAN_SINGLE = "barcodeScanSingle";
|
||||
// Barcode scanner types
|
||||
const int BARCODE_CONTROLLER_CAMERA = 0;
|
||||
const int BARCODE_CONTROLLER_WEDGE = 1;
|
||||
const int BARCODE_CONTROLLER_INTENT = 2;
|
||||
|
||||
// Whether one-time intent wedge detection has run
|
||||
const String INV_BARCODE_WEDGE_DETECTED = "barcodeWedgeDetected";
|
||||
|
||||
/*
|
||||
* Class for storing InvenTree preferences in a NoSql DB
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import "dart:io";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
|
||||
@@ -112,6 +114,8 @@ class _InvenTreeBarcodeSettingsState
|
||||
Widget? barcodeInputIcon;
|
||||
|
||||
switch (barcodeScanType) {
|
||||
case BARCODE_CONTROLLER_INTENT:
|
||||
barcodeInputIcon = Icon(TablerIcons.arrow_badge_right);
|
||||
case BARCODE_CONTROLLER_WEDGE:
|
||||
barcodeInputIcon = Icon(Icons.barcode_reader);
|
||||
case BARCODE_CONTROLLER_CAMERA:
|
||||
@@ -143,6 +147,12 @@ class _InvenTreeBarcodeSettingsState
|
||||
subtitle: Text(L10().scannerExternalDetail),
|
||||
leading: Icon(Icons.barcode_reader),
|
||||
),
|
||||
if (Platform.isAndroid)
|
||||
ListTile(
|
||||
title: Text(L10().scannerIntent),
|
||||
subtitle: Text(L10().scannerIntentDetail),
|
||||
leading: Icon(TablerIcons.arrow_badge_right),
|
||||
),
|
||||
],
|
||||
onSelected: (idx) async {
|
||||
barcodeScanType = idx as int;
|
||||
|
||||
Reference in New Issue
Block a user