huge update, such refactoring much doku (noch nich alles), 6.2.3 compatibility, dependency updates
This commit is contained in:
parent
2c9d1b7d75
commit
5418e49ff3
37
README.md
37
README.md
|
@ -1,9 +1,7 @@
|
|||
# SnipeIT Scanner App
|
||||
|
||||
This is a scanner app for [SnipeIT](https://snipeitapp.com/).
|
||||
|
||||
## Creating JSON File for User Dropdown
|
||||
|
||||
```json
|
||||
// https://snipe.example.com/scanner_users.json
|
||||
[
|
||||
|
@ -13,7 +11,36 @@ This is a scanner app for [SnipeIT](https://snipeitapp.com/).
|
|||
}
|
||||
]
|
||||
```
|
||||
The Key needs to be an encrypted API Key from Snipe. It's encrypted using AES in ECB mode using a numeric secret. The PIN in the login screen is prefixed with 0's until it has a length of 32.
|
||||
A script to generate a user entry for the JSON file lies [Here](encryptKey)
|
||||
The Key needs to be an encrypted API Key from SnipeIT. It's encrypted using AES in ECB mode using a numeric secret. The PIN in the login screen is prefixed with 0's until it has a length of 32.
|
||||
A script to generate a user entry for the JSON file lies [here](encryptKey)
|
||||
|
||||
I would not recommend to make this file available to the internet, as it uses weak pin codes as encryption for your access tokens. It's meant to be only available within your company network.
|
||||
I would not recommend to make this file available to the internet, as it uses weak pin codes as encryption for your access tokens. It's meant to be only available within your company network.
|
||||
|
||||
## Development
|
||||
To develop for this project setup an editor according to [this](https://docs.flutter.dev/get-started/editor?tab=vscode)
|
||||
After you have set up your editor successfully you need to run the [initialize.sh](initialize.sh) script to initialize this project. It installs all the dependencies.
|
||||
|
||||
## New Release
|
||||
To release a new version open [pubspec.yaml](pubspec.yaml) and edit the version number. Depending on your changes, increase either the major, minor or bugfix number. Also you need to increase the build number by one.
|
||||
|
||||
e.g.
|
||||
current version string: ```version: 1.3.5+16```
|
||||
next version: ```version: 1.3.6```
|
||||
next build number: ```17```
|
||||
so your next tag should read ```version: 1.3.6+17```
|
||||
|
||||
After this commit your code to the repository 🤓.
|
||||
|
||||
Run the [build.sh](build.sh) script (if you are on linux or macos)
|
||||
It currently runs ```flutter build appbundle --release``` but if in the future this app needs more steps to build, it can be simply added to the build.sh
|
||||
|
||||
**google REQUIRES build numbers to be unique when uploading to the play store!**
|
||||
|
||||
Finally your new build (for the android app) resides in [build/app/outputs/bundle](build/app/outputs/bundle) which you can then distribute to your devices.
|
||||
|
||||
|
||||
|
||||
todo:
|
||||
dropdown_search *2.0.1 *2.0.1 5.0.6 5.0.6
|
||||
http *0.13.6 *0.13.6 1.2.0 1.2.0
|
||||
screen_state *2.0.0 *2.0.0 3.0.1 3.0.1
|
|
@ -8,7 +8,7 @@ if (localPropertiesFile.exists()) {
|
|||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
|
@ -50,8 +50,8 @@ android {
|
|||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.itcreatesmedia.snipe_scanner"
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
targetSdkVersion 33 ///flutter.targetSdkVersion
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 33
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ class DWInterface() {
|
|||
const val DATAWEDGE_SCAN_EXTRA_LABEL_TYPE = "com.symbol.datawedge.label_type"
|
||||
const val DATAWEDGE_SEND_CREATE_PROFILE = "com.symbol.datawedge.api.CREATE_PROFILE"
|
||||
const val DATAWEDGE_SEND_SET_CONFIG = "com.symbol.datawedge.api.SET_CONFIG"
|
||||
const val DATAWEDGE_SEND_GET_CONFIG = "com.symbol.datawedge.api.GET_CONFIG"
|
||||
}
|
||||
|
||||
fun sendCommandString(context: Context, command: String, parameter: String, sendResult: Boolean = false) {
|
||||
|
|
|
@ -14,7 +14,7 @@ import java.util.*
|
|||
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
private val COMMAND_CHANNEL = "com.itcreatesmedia.snipe_scanner/command"
|
||||
private val COMMAND_CHANNEL = "com.itcreatesmedia.snipe_scanner/command"
|
||||
private val SCAN_CHANNEL = "com.itcreatesmedia.snipe_scanner/scan"
|
||||
private val PROFILE_INTENT_ACTION = "com.itcreatesmedia.snipe_scanner.SCAN"
|
||||
private val PROFILE_INTENT_BROADCAST = "2"
|
||||
|
@ -50,11 +50,22 @@ class MainActivity: FlutterActivity() {
|
|||
val command: String = arguments.get("command") as String
|
||||
val parameter: String = arguments.get("parameter") as String
|
||||
dwInterface.sendCommandString(applicationContext, command, parameter)
|
||||
// result.success(0); // DataWedge does not return responses
|
||||
result.success(0); // DataWedge does not return responses
|
||||
}
|
||||
else if (call.method == "createDataWedgeProfile")
|
||||
{
|
||||
createDataWedgeProfile(call.arguments.toString())
|
||||
result.success(0);
|
||||
}
|
||||
else if (call.method == "updateDataWedgeProfile")
|
||||
{
|
||||
updateDataWedgeProfile(call.arguments.toString())
|
||||
result.success(0);
|
||||
}
|
||||
else if (call.method == "getDataWedgeProfile")
|
||||
{
|
||||
getDataWedgeProfile(call.arguments.toString())
|
||||
result.success(0);
|
||||
}
|
||||
else {
|
||||
result.notImplemented()
|
||||
|
@ -81,11 +92,7 @@ class MainActivity: FlutterActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDataWedgeProfile(profileName: String) {
|
||||
// Create and configure the DataWedge profile associated with this application
|
||||
// For readability's sake, I have not defined each of the keys in the DWInterface file
|
||||
dwInterface.sendCommandString(this, DWInterface.DATAWEDGE_SEND_CREATE_PROFILE, profileName)
|
||||
private fun updateDataWedgeProfile(profileName: String) {
|
||||
val profileConfig = Bundle()
|
||||
profileConfig.putString("PROFILE_NAME", profileName)
|
||||
profileConfig.putString("PROFILE_ENABLED", "true") // These are all strings
|
||||
|
@ -94,6 +101,10 @@ class MainActivity: FlutterActivity() {
|
|||
barcodeConfig.putString("PLUGIN_NAME", "BARCODE")
|
||||
barcodeConfig.putString("RESET_CONFIG", "true") // This is the default but never hurts to specify
|
||||
val barcodeProps = Bundle()
|
||||
barcodeProps.putString("decode_haptic_feedback", "true")
|
||||
barcodeProps.putString("decoding_led_feedback", "true")
|
||||
barcodeProps.putString("decode_audio_feedback_uri", "")
|
||||
barcodeProps.putString("scanner_selection", "auto")
|
||||
barcodeConfig.putBundle("PARAM_LIST", barcodeProps)
|
||||
profileConfig.putBundle("PLUGIN_CONFIG", barcodeConfig)
|
||||
val appConfig = Bundle()
|
||||
|
@ -106,12 +117,16 @@ class MainActivity: FlutterActivity() {
|
|||
val intentConfig = Bundle()
|
||||
intentConfig.putString("PLUGIN_NAME", "INTENT")
|
||||
intentConfig.putString("RESET_CONFIG", "true")
|
||||
dwInterface.sendCommandBundle(this, DWInterface.DATAWEDGE_SEND_SET_CONFIG, profileConfig)
|
||||
profileConfig.remove("PLUGIN_CONFIG")
|
||||
val intentProps = Bundle()
|
||||
intentProps.putString("intent_output_enabled", "true")
|
||||
intentProps.putString("intent_action", PROFILE_INTENT_ACTION)
|
||||
intentProps.putString("intent_delivery", PROFILE_INTENT_BROADCAST) // "2"
|
||||
intentConfig.putBundle("PARAM_LIST", intentProps)
|
||||
profileConfig.putBundle("PLUGIN_CONFIG", intentConfig)
|
||||
dwInterface.sendCommandBundle(this, DWInterface.DATAWEDGE_SEND_SET_CONFIG, profileConfig)
|
||||
profileConfig.remove("PLUGIN_CONFIG")
|
||||
val keyboardConfig = Bundle()
|
||||
keyboardConfig.putString("PLUGIN_NAME", "KEYSTROKE")
|
||||
keyboardConfig.putString("RESET_CONFIG", "true")
|
||||
|
@ -121,4 +136,14 @@ class MainActivity: FlutterActivity() {
|
|||
profileConfig.putBundle("PLUGIN_CONFIG", keyboardConfig)
|
||||
dwInterface.sendCommandBundle(this, DWInterface.DATAWEDGE_SEND_SET_CONFIG, profileConfig)
|
||||
}
|
||||
private fun createDataWedgeProfile(profileName: String) {
|
||||
// Create and configure the DataWedge profile associated with this application
|
||||
// For readability's sake, I have not defined each of the keys in the DWInterface file
|
||||
dwInterface.sendCommandString(this, DWInterface.DATAWEDGE_SEND_CREATE_PROFILE, profileName)
|
||||
updateDataWedgeProfile(profileName)
|
||||
}
|
||||
private fun getDataWedgeProfile(profileName: String) {
|
||||
println("get config from datawedge")
|
||||
dwInterface.sendCommandString(this, DWInterface.DATAWEDGE_SEND_GET_CONFIG, profileName)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,6 @@ subprojects {
|
|||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -19,7 +19,7 @@ then
|
|||
fi
|
||||
|
||||
if [ $KEYLENGTH -gt 32 ]; then
|
||||
echo "Key is to long"
|
||||
echo "Key is too long"
|
||||
exit 1
|
||||
fi
|
||||
while [ $KEYLENGTH -lt 32 ]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:html_unescape/html_unescape.dart';
|
||||
|
||||
String truncate(String text, {int length = 20, String omission = '...'}) {
|
||||
if (length >= text.length) {
|
||||
|
@ -7,7 +8,7 @@ String truncate(String text, {int length = 20, String omission = '...'}) {
|
|||
return text.replaceRange(length, text.length, omission);
|
||||
}
|
||||
|
||||
Widget itemRow(String barcode, BuildContext context,
|
||||
Widget assetRow(String barcode, BuildContext context,
|
||||
{String? name,
|
||||
String? note,
|
||||
bool? status,
|
||||
|
@ -43,7 +44,7 @@ Widget itemRow(String barcode, BuildContext context,
|
|||
),
|
||||
InkWell(
|
||||
child: Text(
|
||||
truncate(name, length: 20),
|
||||
truncate(HtmlUnescape().convert(name), length: 20),
|
||||
style: const TextStyle(color: Colors.lightBlue),
|
||||
),
|
||||
onTap: () => {
|
||||
|
@ -76,9 +77,9 @@ Widget itemRow(String barcode, BuildContext context,
|
|||
Container(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: const SizedBox(
|
||||
child: CircularProgressIndicator(),
|
||||
height: 16.0,
|
||||
width: 16.0,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
@ -97,7 +98,7 @@ Widget itemRow(String barcode, BuildContext context,
|
|||
"Checkin from",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(truncate(checkinFrom)),
|
||||
Text(truncate(HtmlUnescape().convert(checkinFrom))),
|
||||
],
|
||||
)),
|
||||
if (checkoutTo != null && checkoutTo != "")
|
||||
|
@ -112,7 +113,7 @@ Widget itemRow(String barcode, BuildContext context,
|
|||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
truncate(checkoutTo),
|
||||
truncate(HtmlUnescape().convert(checkoutTo)),
|
||||
)
|
||||
],
|
||||
)),
|
||||
|
@ -126,7 +127,7 @@ Widget itemRow(String barcode, BuildContext context,
|
|||
"Note",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(truncate(note, length: 30))
|
||||
Text(truncate(HtmlUnescape().convert(note), length: 30))
|
||||
],
|
||||
))
|
||||
],
|
|
@ -1,99 +0,0 @@
|
|||
import 'package:dropdown_search/dropdown_search.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:snipe_scanner/model/app_state.dart';
|
||||
import 'package:snipe_scanner/model/asset.dart';
|
||||
import 'package:snipe_scanner/service/asset.dart';
|
||||
|
||||
class AssetSelector extends StatefulWidget {
|
||||
final Function(int) cb;
|
||||
const AssetSelector({Key? key, required this.cb}) : super(key: key);
|
||||
|
||||
@override
|
||||
_AssetSelectorState createState() => _AssetSelectorState();
|
||||
}
|
||||
|
||||
class _AssetSelectorState extends State<AssetSelector> {
|
||||
List<Asset> _assets = [];
|
||||
bool _loading = true;
|
||||
int _dst = 0;
|
||||
void _get() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
});
|
||||
var token = Provider.of<AppState>(context, listen: false).token;
|
||||
var assets = await getAssets(token);
|
||||
setState(() {
|
||||
_assets = assets;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_get();
|
||||
}
|
||||
|
||||
void _setDst(int dst) {
|
||||
setState(() {
|
||||
_dst = dst;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Size size = MediaQuery.of(context).size;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Row(
|
||||
children: [
|
||||
Row(children: [
|
||||
SizedBox(
|
||||
width: size.width - 140,
|
||||
child: DropdownSearch<Asset>(
|
||||
mode: Mode.DIALOG,
|
||||
dropdownSearchDecoration:
|
||||
const InputDecoration(labelText: 'Assets'),
|
||||
showAsSuffixIcons: true,
|
||||
showSearchBox: true,
|
||||
showSelectedItems: true,
|
||||
items: _assets,
|
||||
itemAsString: (Asset? a) =>
|
||||
(a?.assetTag ?? '') + ' ' + (a?.name ?? ''),
|
||||
onChanged: (c) => _setDst(c!.id!),
|
||||
filterFn: (Asset? asset, String? search) {
|
||||
var matched = search == null
|
||||
? true
|
||||
: asset!.name
|
||||
?.toLowerCase()
|
||||
.contains(search.toLowerCase());
|
||||
return matched ?? false;
|
||||
},
|
||||
compareFn: (i, sI) => i?.id == sI?.id,
|
||||
),
|
||||
),
|
||||
if (_loading)
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
if (!_loading)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Container(
|
||||
color: Colors.black12,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_right_alt),
|
||||
tooltip: 'Select asset',
|
||||
onPressed: () => widget.cb(_dst),
|
||||
),
|
||||
),
|
||||
)
|
||||
])
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,14 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
// Big button represents the big buttons shown on the dashboard. It offers a title and a clicked callback function to be set
|
||||
Widget bigButton(String title, void Function()? cb) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 10),
|
||||
child: ElevatedButton(
|
||||
onPressed: cb,
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.zero))),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: 50.0,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info/package_info.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
class BuildInfo extends StatefulWidget {
|
||||
const BuildInfo({Key? key}) : super(key: key);
|
||||
const BuildInfo({super.key});
|
||||
|
||||
@override
|
||||
_BuildInfoState createState() => _BuildInfoState();
|
||||
State<BuildInfo> createState() => _BuildInfoState();
|
||||
}
|
||||
|
||||
class _BuildInfoState extends State<BuildInfo> {
|
||||
|
@ -18,7 +18,7 @@ class _BuildInfoState extends State<BuildInfo> {
|
|||
|
||||
void getInfo() async {
|
||||
var info = await PackageInfo.fromPlatform();
|
||||
setState(() => {_packageInfo = info});
|
||||
setState(() => _packageInfo = info);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -27,11 +27,7 @@ class _BuildInfoState extends State<BuildInfo> {
|
|||
return const Text("dev version");
|
||||
} else {
|
||||
return Text(
|
||||
"v" +
|
||||
_packageInfo!.version +
|
||||
(_packageInfo!.buildNumber != ""
|
||||
? "+" + _packageInfo!.buildNumber
|
||||
: ""),
|
||||
"Compatible SnipeIT: 6.2.3 App: v${_packageInfo!.version}${_packageInfo!.buildNumber != "" ? "+${_packageInfo!.buildNumber}" : ""}",
|
||||
style: const TextStyle(fontSize: 10),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import 'package:dropdown_search/dropdown_search.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:html_unescape/html_unescape.dart';
|
||||
import 'package:snipe_scanner/model/comparable_with_id.dart';
|
||||
|
||||
class DropdownSearchInput<T extends ComparableWithId> extends StatelessWidget {
|
||||
final void Function(int?) cb;
|
||||
final String label;
|
||||
final Future<List<T>> Function(String) getItems;
|
||||
const DropdownSearchInput(
|
||||
{super.key, required this.cb, required this.label, required this.getItems});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownSearch<T>(
|
||||
dropdownDecoratorProps: DropDownDecoratorProps(
|
||||
dropdownSearchDecoration: InputDecoration(labelText: this.label)),
|
||||
popupProps: const PopupProps.modalBottomSheet(
|
||||
showSelectedItems: true,
|
||||
showSearchBox: true,
|
||||
isFilterOnline: true,
|
||||
searchDelay: Duration(milliseconds: 300)),
|
||||
asyncItems: this.getItems,
|
||||
itemAsString: (ComparableWithId? a) =>
|
||||
HtmlUnescape().convert(a!.getName()),
|
||||
onChanged: (c) => this.cb(c!.id),
|
||||
compareFn: (a, b) => a.equals(b));
|
||||
}
|
||||
}
|
|
@ -54,7 +54,8 @@ Widget historyRow(Activity activity, BuildContext context) {
|
|||
),
|
||||
InkWell(
|
||||
child: Text(
|
||||
activity.admin!.name!,
|
||||
HtmlUnescape()
|
||||
.convert(activity.admin!.name!),
|
||||
style: const TextStyle(
|
||||
color: Colors.lightBlue),
|
||||
),
|
||||
|
@ -97,7 +98,8 @@ Widget historyRow(Activity activity, BuildContext context) {
|
|||
),
|
||||
InkWell(
|
||||
child: Text(
|
||||
activity.target!.name!,
|
||||
HtmlUnescape()
|
||||
.convert(activity.target!.name!),
|
||||
style: const TextStyle(
|
||||
color: Colors.lightBlue),
|
||||
),
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class AssetHistoryView extends StatefulWidget {
|
||||
const AssetHistoryView({Key? key}) : super(key: key);
|
||||
const AssetHistoryView({super.key});
|
||||
|
||||
@override
|
||||
_AssetHistoryViewState createState() => _AssetHistoryViewState();
|
||||
State<AssetHistoryView> createState() => _AssetHistoryViewState();
|
||||
}
|
||||
|
||||
class _AssetHistoryViewState extends State<AssetHistoryView> {
|
||||
|
|
|
@ -1,44 +1,38 @@
|
|||
import 'package:dropdown_search/dropdown_search.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:snipe_scanner/components/dropdown_search_input.dart';
|
||||
import 'package:snipe_scanner/model/app_state.dart';
|
||||
import 'package:snipe_scanner/model/location.dart';
|
||||
import 'package:snipe_scanner/service/location.dart';
|
||||
|
||||
class LocationSelector extends StatefulWidget {
|
||||
final Function(int) cb;
|
||||
const LocationSelector({Key? key, required this.cb}) : super(key: key);
|
||||
final bool instant;
|
||||
const LocationSelector({super.key, required this.cb, required this.instant});
|
||||
|
||||
@override
|
||||
_LocationSelectorState createState() => _LocationSelectorState();
|
||||
State<LocationSelector> createState() => _LocationSelectorState();
|
||||
}
|
||||
|
||||
class _LocationSelectorState extends State<LocationSelector> {
|
||||
List<Location> _items = [];
|
||||
bool _loading = true;
|
||||
int _dst = -1;
|
||||
void _getItems() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
});
|
||||
Future<List<Location>> _getItems(String filter) async {
|
||||
var token = Provider.of<AppState>(context, listen: false).token;
|
||||
var locations = await getLocations(token);
|
||||
setState(() {
|
||||
_items = locations;
|
||||
_loading = false;
|
||||
});
|
||||
return await getLocations(token, filter);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_getItems();
|
||||
}
|
||||
|
||||
void _setDst(int dst) {
|
||||
void _setDst(int? dst) {
|
||||
setState(() {
|
||||
_dst = dst;
|
||||
_dst = dst ?? -1;
|
||||
});
|
||||
if (widget.instant) {
|
||||
widget.cb(dst ?? -1);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -50,33 +44,13 @@ class _LocationSelectorState extends State<LocationSelector> {
|
|||
children: [
|
||||
Row(children: [
|
||||
SizedBox(
|
||||
width: size.width - 140,
|
||||
child: DropdownSearch<Location>(
|
||||
mode: Mode.BOTTOM_SHEET,
|
||||
dropdownSearchDecoration:
|
||||
const InputDecoration(labelText: 'Select location'),
|
||||
showAsSuffixIcons: true,
|
||||
showSearchBox: true,
|
||||
showSelectedItems: true,
|
||||
items: _items,
|
||||
itemAsString: (Location? l) => l?.name ?? '',
|
||||
onChanged: (c) => _setDst(c!.id),
|
||||
filterFn: (Location? l, String? search) {
|
||||
var matched = search == null
|
||||
? true
|
||||
: l!.name.toLowerCase().contains(search.toLowerCase());
|
||||
return matched;
|
||||
},
|
||||
compareFn: (i, sI) => i?.id == sI?.id,
|
||||
),
|
||||
),
|
||||
if (_loading)
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
if (!_loading)
|
||||
width: size.width - 80 - (widget.instant ? 0 : 60),
|
||||
child: DropdownSearchInput<Location>(
|
||||
cb: _setDst,
|
||||
label: 'Select location',
|
||||
getItems: _getItems,
|
||||
)),
|
||||
if (!widget.instant)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Container(
|
||||
|
|
|
@ -3,10 +3,10 @@ import 'package:snipe_scanner/main.dart';
|
|||
|
||||
class ManualInput extends StatefulWidget {
|
||||
final Function(String) cb;
|
||||
const ManualInput({Key? key, required this.cb}) : super(key: key);
|
||||
const ManualInput({super.key, required this.cb});
|
||||
|
||||
@override
|
||||
_ManualInputState createState() => _ManualInputState();
|
||||
State<ManualInput> createState() => _ManualInputState();
|
||||
}
|
||||
|
||||
class _ManualInputState extends State<ManualInput> {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:html_unescape/html_unescape.dart';
|
||||
import 'package:snipe_scanner/model/asset.dart';
|
||||
|
||||
Widget searchItemRow(Asset asset, BuildContext context,
|
||||
|
@ -44,27 +45,28 @@ Widget searchItemRow(Asset asset, BuildContext context,
|
|||
Text(asset.assetTag!)
|
||||
],
|
||||
)),
|
||||
if (asset.name != null)
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10, bottom: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"Name",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
Wrap(
|
||||
direction: Axis.horizontal,
|
||||
children: [Text(asset.name!)],
|
||||
)
|
||||
],
|
||||
)),
|
||||
)
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 10, bottom: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"Name",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
Wrap(
|
||||
direction: Axis.horizontal,
|
||||
children: [
|
||||
Text(HtmlUnescape()
|
||||
.convert(asset.getShortName()))
|
||||
],
|
||||
)
|
||||
],
|
||||
)),
|
||||
)
|
||||
]),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
|
@ -81,7 +83,8 @@ Widget searchItemRow(Asset asset, BuildContext context,
|
|||
TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
asset.assignedTo!.name!,
|
||||
HtmlUnescape()
|
||||
.convert(asset.assignedTo!.name!),
|
||||
)
|
||||
],
|
||||
))
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:dropdown_search/dropdown_search.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:snipe_scanner/components/dropdown_search_input.dart';
|
||||
import 'package:snipe_scanner/model/app_state.dart';
|
||||
import 'package:snipe_scanner/model/asset.dart';
|
||||
import 'package:snipe_scanner/model/location.dart';
|
||||
|
@ -12,63 +12,33 @@ import '../service/location.dart';
|
|||
|
||||
class TargetSelector extends StatefulWidget {
|
||||
final Function(String, int) cb;
|
||||
const TargetSelector({Key? key, required this.cb}) : super(key: key);
|
||||
const TargetSelector({super.key, required this.cb});
|
||||
|
||||
@override
|
||||
_TargetSelectorState createState() => _TargetSelectorState();
|
||||
State<TargetSelector> createState() => _TargetSelectorState();
|
||||
}
|
||||
|
||||
class _TargetSelectorState extends State<TargetSelector> {
|
||||
String _type = "user";
|
||||
List<User> _users = [];
|
||||
List<Location> _locations = [];
|
||||
List<Asset> _assets = [];
|
||||
bool _usersLoading = true;
|
||||
bool _locationsLoading = true;
|
||||
bool _assetsLoading = true;
|
||||
int _dst = 0;
|
||||
void _getUsers() async {
|
||||
setState(() {
|
||||
_usersLoading = true;
|
||||
});
|
||||
Future<List<User>> _getUsers(String search) async {
|
||||
var token = Provider.of<AppState>(context, listen: false).token;
|
||||
var users = await getUsers(token);
|
||||
setState(() {
|
||||
_users = users;
|
||||
_usersLoading = false;
|
||||
});
|
||||
return await getUsers(token, search);
|
||||
}
|
||||
|
||||
void _getLocations() async {
|
||||
setState(() {
|
||||
_locationsLoading = true;
|
||||
});
|
||||
Future<List<Location>> _getLocations(String search) async {
|
||||
var token = Provider.of<AppState>(context, listen: false).token;
|
||||
var locations = await getLocations(token);
|
||||
setState(() {
|
||||
_locations = locations;
|
||||
_locationsLoading = false;
|
||||
});
|
||||
return await getLocations(token);
|
||||
}
|
||||
|
||||
void _getAssets() async {
|
||||
setState(() {
|
||||
_assetsLoading = true;
|
||||
});
|
||||
Future<List<Asset>> _getAssets(String filter) async {
|
||||
var token = Provider.of<AppState>(context, listen: false).token;
|
||||
var assets = await getAssets(token);
|
||||
setState(() {
|
||||
_assets = assets;
|
||||
_assetsLoading = false;
|
||||
});
|
||||
return await getAssets(token, filter);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_getUsers();
|
||||
_getLocations();
|
||||
_getAssets();
|
||||
}
|
||||
|
||||
void _setDst(int dst) {
|
||||
|
@ -114,107 +84,35 @@ class _TargetSelectorState extends State<TargetSelector> {
|
|||
if (_type == "user")
|
||||
Row(children: [
|
||||
SizedBox(
|
||||
width: _usersLoading ? size.width - 200 : size.width - 180,
|
||||
child: DropdownSearch<User>(
|
||||
mode: Mode.DIALOG,
|
||||
dropdownSearchDecoration:
|
||||
const InputDecoration(labelText: 'Select user'),
|
||||
showAsSuffixIcons: true,
|
||||
showSearchBox: true,
|
||||
showSelectedItems: true,
|
||||
items: _users,
|
||||
itemAsString: (User? u) => u?.name ?? '',
|
||||
onChanged: (c) => _setDst(c!.id),
|
||||
filterFn: (User? user, String? search) {
|
||||
var matched = search == null
|
||||
? true
|
||||
: user!.name
|
||||
.toLowerCase()
|
||||
.contains(search.toLowerCase());
|
||||
return matched;
|
||||
},
|
||||
compareFn: (i, sI) => i?.id == sI?.id,
|
||||
),
|
||||
),
|
||||
if (_usersLoading)
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
width: size.width - 180,
|
||||
child: DropdownSearchInput<User>(
|
||||
cb: (c) => _setDst(c!),
|
||||
label: 'Select user',
|
||||
getItems: _getUsers,
|
||||
)),
|
||||
]),
|
||||
if (_type == "location")
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width:
|
||||
_locationsLoading ? size.width - 220 : size.width - 200,
|
||||
child: DropdownSearch<Location>(
|
||||
mode: Mode.DIALOG,
|
||||
dropdownSearchDecoration:
|
||||
const InputDecoration(labelText: 'Select location'),
|
||||
showAsSuffixIcons: true,
|
||||
showSearchBox: true,
|
||||
showSelectedItems: true,
|
||||
items: _locations,
|
||||
itemAsString: (Location? l) => l?.name ?? '',
|
||||
onChanged: (c) => _setDst(c!.id),
|
||||
filterFn: (Location? loc, String? search) {
|
||||
var matched = search == null
|
||||
? true
|
||||
: loc!.name
|
||||
.toLowerCase()
|
||||
.contains(search.toLowerCase());
|
||||
return matched;
|
||||
},
|
||||
compareFn: (i, sI) => i?.id == sI?.id,
|
||||
),
|
||||
),
|
||||
if (_locationsLoading)
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
width: size.width - 180,
|
||||
child: DropdownSearchInput<Location>(
|
||||
cb: (c) => _setDst(c!),
|
||||
label: 'Select location',
|
||||
getItems: _getLocations,
|
||||
)),
|
||||
],
|
||||
),
|
||||
if (_type == "asset")
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width:
|
||||
_locationsLoading ? size.width - 220 : size.width - 200,
|
||||
child: DropdownSearch<Asset>(
|
||||
mode: Mode.DIALOG,
|
||||
dropdownSearchDecoration:
|
||||
const InputDecoration(labelText: 'Select asset'),
|
||||
showAsSuffixIcons: true,
|
||||
showSearchBox: true,
|
||||
showSelectedItems: true,
|
||||
items: _assets,
|
||||
itemAsString: (Asset? a) =>
|
||||
(a?.assetTag ?? '') + ' ' + (a?.name ?? ''),
|
||||
onChanged: (c) => _setDst(c!.id!),
|
||||
filterFn: (Asset? as, String? search) {
|
||||
var matched = search == null
|
||||
? true
|
||||
: ((as!.name ?? "")
|
||||
.toLowerCase()
|
||||
.contains(search.toLowerCase()) ||
|
||||
(as.assetTag ?? "")
|
||||
.toLowerCase()
|
||||
.contains(search.toLowerCase()));
|
||||
return matched;
|
||||
},
|
||||
compareFn: (i, sI) => i?.id == sI?.id,
|
||||
),
|
||||
),
|
||||
if (_locationsLoading)
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
width: size.width - 180,
|
||||
child: DropdownSearchInput<Asset>(
|
||||
cb: (c) => _setDst(c!),
|
||||
label: 'Select asset',
|
||||
getItems: _getAssets,
|
||||
)),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:html_unescape/html_unescape.dart';
|
||||
|
||||
class TextRow extends StatelessWidget {
|
||||
final String title;
|
||||
final String text;
|
||||
const TextRow({Key? key, required this.title, required this.text})
|
||||
: super(key: key);
|
||||
const TextRow({super.key, required this.title, required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -19,7 +19,7 @@ class TextRow extends StatelessWidget {
|
|||
child: Container(
|
||||
padding: const EdgeInsets.only(right: 20, bottom: 10),
|
||||
child: Text(
|
||||
text,
|
||||
HtmlUnescape().convert(text),
|
||||
style: const TextStyle(fontSize: 16),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
|
|
|
@ -1,43 +1,33 @@
|
|||
import 'package:dropdown_search/dropdown_search.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:snipe_scanner/components/dropdown_search_input.dart';
|
||||
import 'package:snipe_scanner/model/app_state.dart';
|
||||
import 'package:snipe_scanner/model/user.dart';
|
||||
import 'package:snipe_scanner/service/users.dart';
|
||||
|
||||
class UserSelector extends StatefulWidget {
|
||||
final Function(int) cb;
|
||||
const UserSelector({Key? key, required this.cb}) : super(key: key);
|
||||
const UserSelector({super.key, required this.cb});
|
||||
|
||||
@override
|
||||
_UserSelectorState createState() => _UserSelectorState();
|
||||
State<UserSelector> createState() => _UserSelectorState();
|
||||
}
|
||||
|
||||
class _UserSelectorState extends State<UserSelector> {
|
||||
List<User> _users = [];
|
||||
bool _usersLoading = true;
|
||||
int _dst = 0;
|
||||
void _getUsers() async {
|
||||
setState(() {
|
||||
_usersLoading = true;
|
||||
});
|
||||
Future<List<User>> _getUsers(String filter) async {
|
||||
var token = Provider.of<AppState>(context, listen: false).token;
|
||||
var users = await getUsers(token);
|
||||
setState(() {
|
||||
_users = users;
|
||||
_usersLoading = false;
|
||||
});
|
||||
return await getUsers(token);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_getUsers();
|
||||
}
|
||||
|
||||
void _setDst(int dst) {
|
||||
void _setDst(int? dst) {
|
||||
setState(() {
|
||||
_dst = dst;
|
||||
_dst = dst ?? 0;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -50,44 +40,23 @@ class _UserSelectorState extends State<UserSelector> {
|
|||
children: [
|
||||
Row(children: [
|
||||
SizedBox(
|
||||
width: size.width - 140,
|
||||
child: DropdownSearch<User>(
|
||||
mode: Mode.DIALOG,
|
||||
dropdownSearchDecoration:
|
||||
const InputDecoration(labelText: 'Get user details'),
|
||||
showAsSuffixIcons: true,
|
||||
showSearchBox: true,
|
||||
showSelectedItems: true,
|
||||
items: _users,
|
||||
itemAsString: (User? u) => u?.name ?? '',
|
||||
onChanged: (c) => _setDst(c!.id),
|
||||
filterFn: (User? user, String? search) {
|
||||
var matched = search == null
|
||||
? true
|
||||
: user!.name.toLowerCase().contains(search.toLowerCase());
|
||||
return matched;
|
||||
},
|
||||
compareFn: (i, sI) => i?.id == sI?.id,
|
||||
),
|
||||
),
|
||||
if (_usersLoading)
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
if (!_usersLoading)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Container(
|
||||
color: Colors.black12,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_right_alt),
|
||||
tooltip: 'Select user',
|
||||
onPressed: () => widget.cb(_dst),
|
||||
),
|
||||
width: size.width - 140,
|
||||
child: DropdownSearchInput<User>(
|
||||
cb: _setDst,
|
||||
label: 'Get user details',
|
||||
getItems: _getUsers,
|
||||
)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Container(
|
||||
color: Colors.black12,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_right_alt),
|
||||
tooltip: 'Select user',
|
||||
onPressed: () => widget.cb(_dst),
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
])
|
||||
],
|
||||
),
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
/*
|
||||
Main file of the app, initializing stuff and setting the routes up.
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:pref/pref.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:screen_state/screen_state.dart';
|
||||
import 'package:snipe_scanner/utils/audio.dart';
|
||||
import 'package:snipe_scanner/utils/zebra_scan.dart';
|
||||
import 'package:snipe_scanner/widgets/audit.dart';
|
||||
import 'package:snipe_scanner/widgets/bulk_checkin.dart';
|
||||
|
@ -13,17 +19,22 @@ import 'package:snipe_scanner/widgets/bulk_checkout.dart';
|
|||
import 'package:snipe_scanner/widgets/checkin_asset.dart';
|
||||
import 'package:snipe_scanner/widgets/checkout_asset.dart';
|
||||
import 'package:snipe_scanner/widgets/dashboard.dart';
|
||||
import 'package:snipe_scanner/widgets/item_detail.dart';
|
||||
import 'package:snipe_scanner/widgets/asset_detail.dart';
|
||||
import 'package:snipe_scanner/widgets/login.dart';
|
||||
import 'package:snipe_scanner/widgets/search_asset.dart';
|
||||
import 'package:snipe_scanner/widgets/user_detail.dart';
|
||||
import 'package:snipe_scanner/widgets/preferences.dart';
|
||||
import 'utils/logging.dart';
|
||||
|
||||
import 'model/app_state.dart';
|
||||
|
||||
// navigator key is used to navigate through the app programatically even without being inside the app state.
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
// route observer is used to observe navigating through the app, it offers functionality for the views to know if they are navigated to..
|
||||
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
|
||||
|
||||
// shared preference service, this holds the preferences and persists them on the phone.
|
||||
late final PrefServiceShared service;
|
||||
|
||||
Future main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
|
@ -37,69 +48,85 @@ Future main() async {
|
|||
'auto_logout_time': 60,
|
||||
'warn_before_checkin_from_asset': false
|
||||
});
|
||||
// init the zebra scanning engine and setup the datawedge profile
|
||||
ZebraScan.init();
|
||||
// init the audio players for the scan responses
|
||||
initAudio();
|
||||
// init the remote logging for debugging purposes
|
||||
initLogger(const MyApp(), navigatorKey);
|
||||
runApp(PrefService(
|
||||
service: service,
|
||||
child: ChangeNotifierProvider(
|
||||
create: (_) => AppState(), child: const MyApp()),
|
||||
service: service));
|
||||
create: (_) => AppState(), child: const MyApp())));
|
||||
}
|
||||
|
||||
// this is the main app widget that gets inserted into the app, it's the startingpoint for everything
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({Key? key}) : super(key: key);
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
_MyAppState createState() => _MyAppState();
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
// used toget the screen state
|
||||
final Screen _screen = Screen();
|
||||
// saves the next logout time
|
||||
DateTime? logoutTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// set everything up to see when the screen gets turned off, to logout the user after a specific timeframe set by the preferences
|
||||
try {
|
||||
_screen.screenStateStream!.listen((ScreenStateEvent e) {
|
||||
if (e == ScreenStateEvent.SCREEN_OFF &&
|
||||
logoutTime == null &&
|
||||
PrefService.of(context).get('auto_logout')) {
|
||||
// autologout is set via preferences and the screen got turned off, so mark the time when the app should be locked
|
||||
Duration duration = Duration(
|
||||
minutes: PrefService.of(context).get('auto_logout_time'));
|
||||
logoutTime = DateTime.now().add(duration);
|
||||
log("Locking in ${duration.toString()} minutes");
|
||||
} else if (e == ScreenStateEvent.SCREEN_UNLOCKED) {
|
||||
// screen got unlocked, check if logoutTime is set, if so compare it to now and logout the user if logoutTime before now. after this set logoutTime to null to prevent further logouts
|
||||
if (logoutTime != null && logoutTime!.isBefore(DateTime.now())) {
|
||||
Provider.of<AppState>(context, listen: false).setToken("");
|
||||
navigatorKey.currentState?.popUntil((route) => route.isFirst);
|
||||
}
|
||||
logoutTime = null;
|
||||
log("unlocked");
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
log(err.toString());
|
||||
} catch (err, stackTrace) {
|
||||
// this catches the error and uses cather2 to ask the user if he wants to report it.
|
||||
Catcher2.reportCheckedError(err, stackTrace);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
// This widget is the root of your application.
|
||||
// This widget is the root of the app.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// set the orientation to always be portrait
|
||||
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
|
||||
// initialize the material app
|
||||
return MaterialApp(
|
||||
title: 'SnipeIT Scanner',
|
||||
debugShowCheckedModeBanner: false,
|
||||
navigatorKey: navigatorKey,
|
||||
theme: ThemeData(
|
||||
primaryColor: const Color(0xFF2661FA),
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
),
|
||||
primaryColor: const Color(0xFF2661FA),
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
bottomSheetTheme: const BottomSheetThemeData(
|
||||
shape: BeveledRectangleBorder(
|
||||
borderRadius: BorderRadiusDirectional.all(Radius.zero)))),
|
||||
navigatorObservers: [routeObserver],
|
||||
// this defines the first view.
|
||||
home: const LoginWidget(),
|
||||
// here are all the views within the app, so you can navigate through them using named routes
|
||||
routes: <String, WidgetBuilder>{
|
||||
'/dashboard': (BuildContext context) => const DashboardWidget(),
|
||||
'/assetDetail': (BuildContext context) => const ItemDetail(),
|
||||
'/assetDetail': (BuildContext context) => const AssetDetail(),
|
||||
'/checkoutAsset': (BuildContext context) => const CheckoutAsset(),
|
||||
'/checkinAsset': (BuildContext context) => const CheckinAsset(),
|
||||
'/bulkCheckin': (BuildContext context) => const BulkCheckin(),
|
||||
|
@ -107,6 +134,7 @@ class _MyAppState extends State<MyApp> {
|
|||
'/searchAsset': (BuildContext context) => const SearchAsset(),
|
||||
'/userDetail': (BuildContext context) => const UserDetail(),
|
||||
'/auditAsset': (BuildContext context) => const Audit(),
|
||||
'/preferences': (BuildContext context) => const PreferencesWidget(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,10 @@ import 'package:snipe_scanner/model/location.dart';
|
|||
|
||||
part 'activity.g.dart';
|
||||
|
||||
// This model represents an activity in the history of e.g. an asset
|
||||
|
||||
// AFTER CHANGING THIS RUN "dart run build_runner build" in the package directory.
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
|
||||
class Activity {
|
||||
Activity(this.id);
|
||||
|
|
|
@ -2,6 +2,10 @@ import 'package:json_annotation/json_annotation.dart';
|
|||
|
||||
part 'admin.g.dart';
|
||||
|
||||
// This is the model for the stripped down user subset thats included in the activity report response
|
||||
|
||||
// AFTER CHANGING THIS RUN "dart run build_runner build" in the package directory.
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
|
||||
class Admin {
|
||||
Admin(this.id);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
// This model represents the app state. It contains session stuff that is needed to get the data from snipe
|
||||
|
||||
class AppState extends ChangeNotifier {
|
||||
String _token = "";
|
||||
String get token => _token;
|
||||
|
|
|
@ -1,23 +1,31 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:snipe_scanner/model/asset_model.dart';
|
||||
import 'package:snipe_scanner/model/assignment.dart';
|
||||
import 'package:snipe_scanner/model/available_actions.dart';
|
||||
import 'package:snipe_scanner/model/comparable_with_id.dart';
|
||||
import 'package:snipe_scanner/model/date.dart';
|
||||
import 'package:snipe_scanner/model/location.dart';
|
||||
import 'package:snipe_scanner/model/status_label.dart';
|
||||
|
||||
part 'asset.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
|
||||
class Asset {
|
||||
Asset(this.id);
|
||||
// This model represents an asset
|
||||
|
||||
int? id;
|
||||
// AFTER CHANGING THIS RUN "dart run build_runner build" in the package directory.
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
|
||||
class Asset implements ComparableWithId {
|
||||
Asset(this.id, this.model);
|
||||
|
||||
@override
|
||||
int id;
|
||||
String? name;
|
||||
String? assetTag;
|
||||
String? image;
|
||||
StatusLabel? statusLabel;
|
||||
Assignment? assignedTo;
|
||||
Location? location;
|
||||
AssetModel model;
|
||||
String? notes;
|
||||
Date? createdAt;
|
||||
Date? updatedAt;
|
||||
|
@ -27,4 +35,16 @@ class Asset {
|
|||
|
||||
factory Asset.fromJson(Map<String, dynamic> json) => _$AssetFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$AssetToJson(this);
|
||||
|
||||
@override
|
||||
String getName() => '${assetTag ?? ''} ${getShortName()}';
|
||||
String getShortName() {
|
||||
if (name?.isEmpty ?? true) {
|
||||
return model.name;
|
||||
}
|
||||
return name!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool equals(ComparableWithId other) => id == other.id;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@ part of 'asset.dart';
|
|||
// **************************************************************************
|
||||
|
||||
Asset _$AssetFromJson(Map<String, dynamic> json) => Asset(
|
||||
json['id'] as int?,
|
||||
json['id'] as int,
|
||||
AssetModel.fromJson(json['model'] as Map<String, dynamic>),
|
||||
)
|
||||
..name = json['name'] as String?
|
||||
..assetTag = json['asset_tag'] as String?
|
||||
|
@ -47,6 +48,7 @@ Map<String, dynamic> _$AssetToJson(Asset instance) => <String, dynamic>{
|
|||
'status_label': instance.statusLabel?.toJson(),
|
||||
'assigned_to': instance.assignedTo?.toJson(),
|
||||
'location': instance.location?.toJson(),
|
||||
'model': instance.model.toJson(),
|
||||
'notes': instance.notes,
|
||||
'created_at': instance.createdAt?.toJson(),
|
||||
'updated_at': instance.updatedAt?.toJson(),
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'asset_model.g.dart';
|
||||
|
||||
// This model represents an asset
|
||||
|
||||
// AFTER CHANGING THIS RUN "dart run build_runner build" in the package directory.
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
|
||||
class AssetModel {
|
||||
AssetModel(this.id, this.name);
|
||||
|
||||
int id;
|
||||
String name;
|
||||
|
||||
factory AssetModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$AssetModelFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$AssetModelToJson(this);
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'asset_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
AssetModel _$AssetModelFromJson(Map<String, dynamic> json) => AssetModel(
|
||||
json['id'] as int,
|
||||
json['name'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AssetModelToJson(AssetModel instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
};
|
|
@ -2,6 +2,10 @@ import 'package:json_annotation/json_annotation.dart';
|
|||
|
||||
part 'assignment.g.dart';
|
||||
|
||||
// This model represents an assignment to an asset, a location or a user
|
||||
|
||||
// AFTER CHANGING THIS RUN "dart run build_runner build" in the package directory.
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
|
||||
class Assignment {
|
||||
Assignment(this.id);
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'available_actions.g.dart';
|
||||
// This model represents all actions that can be done by this user
|
||||
|
||||
// AFTER CHANGING THIS RUN "dart run build_runner build" in the package directory.
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
|
||||
class AvailableActions {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
abstract class ComparableWithId {
|
||||
int id = 0;
|
||||
String getName();
|
||||
bool equals(ComparableWithId other);
|
||||
}
|
|
@ -2,6 +2,10 @@ import 'package:json_annotation/json_annotation.dart';
|
|||
|
||||
part 'date.g.dart';
|
||||
|
||||
// This model represents the date objects that are contained in several responses
|
||||
|
||||
// AFTER CHANGING THIS RUN "dart run build_runner build" in the package directory.
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
|
||||
class Date {
|
||||
Date(this.datetime, this.formatted);
|
||||
|
|
|
@ -1,15 +1,26 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:snipe_scanner/model/comparable_with_id.dart';
|
||||
|
||||
part 'location.g.dart';
|
||||
|
||||
// This model represents a location
|
||||
|
||||
// AFTER CHANGING THIS RUN "dart run build_runner build" in the package directory.
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
|
||||
class Location {
|
||||
class Location implements ComparableWithId {
|
||||
Location(this.id, this.name);
|
||||
|
||||
@override
|
||||
int id;
|
||||
String name;
|
||||
|
||||
factory Location.fromJson(Map<String, dynamic> json) =>
|
||||
_$LocationFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$LocationToJson(this);
|
||||
|
||||
@override
|
||||
String getName() => name;
|
||||
@override
|
||||
bool equals(ComparableWithId other) => id == other.id;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,11 @@ import 'package:json_annotation/json_annotation.dart';
|
|||
|
||||
part 'login_user.g.dart';
|
||||
|
||||
// This is the model to get a scanner user list fromt the specified json file
|
||||
// it contains the a name for the user (which does not need to be equal to the snipe username) and a weakly encrypted api token
|
||||
|
||||
// AFTER CHANGING THIS RUN "dart run build_runner build" in the package directory.
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
|
||||
class LoginUser {
|
||||
LoginUser(this.name, this.token);
|
||||
|
|
|
@ -2,6 +2,10 @@ import 'package:json_annotation/json_annotation.dart';
|
|||
|
||||
part 'response_status.g.dart';
|
||||
|
||||
// This model represents a response status, also containing the data for successful requests
|
||||
|
||||
// AFTER CHANGING THIS RUN "dart run build_runner build" in the package directory.
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
|
||||
class ResponseStatus {
|
||||
ResponseStatus(this.status, this.messages);
|
||||
|
|
|
@ -2,6 +2,10 @@ import 'package:json_annotation/json_annotation.dart';
|
|||
|
||||
part 'status_label.g.dart';
|
||||
|
||||
// This model represents a status label
|
||||
|
||||
// AFTER CHANGING THIS RUN "dart run build_runner build" in the package directory.
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
|
||||
class StatusLabel {
|
||||
StatusLabel(this.id, this.name);
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:snipe_scanner/model/comparable_with_id.dart';
|
||||
import 'package:snipe_scanner/model/date.dart';
|
||||
|
||||
part 'user.g.dart';
|
||||
|
||||
// This model represents a user
|
||||
|
||||
// AFTER CHANGING THIS RUN "dart run build_runner build" in the package directory.
|
||||
|
||||
@JsonSerializable(
|
||||
fieldRename: FieldRename.snake, explicitToJson: true, includeIfNull: false)
|
||||
class User {
|
||||
class User implements ComparableWithId {
|
||||
User(
|
||||
this.id,
|
||||
this.avatar,
|
||||
|
@ -22,6 +27,7 @@ class User {
|
|||
this.updatedAt,
|
||||
this.lastLogin);
|
||||
|
||||
@override
|
||||
int id;
|
||||
String avatar;
|
||||
String name;
|
||||
|
@ -39,4 +45,8 @@ class User {
|
|||
|
||||
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$UserToJson(this);
|
||||
@override
|
||||
String getName() => name;
|
||||
@override
|
||||
bool equals(ComparableWithId other) => id == other.id;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
/*
|
||||
Here are all the http requests to handle assets
|
||||
*/
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
|
@ -6,6 +9,7 @@ import 'package:snipe_scanner/model/activity.dart';
|
|||
import 'package:snipe_scanner/model/asset.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:snipe_scanner/model/response_status.dart';
|
||||
import 'package:snipe_scanner/service/login_user.dart';
|
||||
|
||||
Future<Asset> getAssetFromTag(String token, String tag) async {
|
||||
final response = await http.get(
|
||||
|
@ -21,9 +25,9 @@ Future<Asset> getAssetFromTag(String token, String tag) async {
|
|||
|
||||
final responseJson = jsonDecode(utf8.decode(response.bodyBytes));
|
||||
if (responseJson["status"].toString() == "error") {
|
||||
throw Exception(responseJson["messages"].toString());
|
||||
throw SnipeException(responseJson["messages"].toString());
|
||||
}
|
||||
return Asset.fromJson(responseJson["rows"][0]);
|
||||
return Asset.fromJson(responseJson);
|
||||
}
|
||||
|
||||
Future<List<Asset>> getAssets(String token, [String search = ""]) async {
|
||||
|
@ -151,6 +155,7 @@ Future<ResponseStatus> auditAsset(String token, String tag,
|
|||
var o = <String, dynamic>{'note': note, 'asset_tag': tag};
|
||||
if (dstId >= 0) {
|
||||
o['location_id'] = dstId;
|
||||
o['update_location'] = 1;
|
||||
}
|
||||
var body = jsonEncode(o);
|
||||
final response =
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
/*
|
||||
Here are all the http requests to handle locations
|
||||
*/
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
/*
|
||||
Here are all the http requests to login a user
|
||||
*/
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
|
@ -11,12 +14,30 @@ class HttpException implements Exception {
|
|||
late int code;
|
||||
late String body;
|
||||
HttpException(int c, String b) {
|
||||
cause = "HTTP Error: " + c.toString();
|
||||
cause = "HTTP Error: $c";
|
||||
code = c;
|
||||
body = b;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "HTTP Error $code: $body";
|
||||
}
|
||||
}
|
||||
|
||||
class SnipeException implements Exception {
|
||||
late String message;
|
||||
SnipeException(String b) {
|
||||
message = b;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
// get the login users using the scanner_users.json
|
||||
Future<List<LoginUser>> getLoginUsers() async {
|
||||
var url = "${service.get('host')}/scanner_users.json";
|
||||
if (service.get<bool>('use_custom_user_url')!) {
|
||||
|
@ -38,7 +59,7 @@ Future<List<LoginUser>> getLoginUsers() async {
|
|||
Future<User> getUser(String token) async {
|
||||
final response = await http
|
||||
.get(Uri.parse("${service.get('host')}/api/v1/users/me"), headers: {
|
||||
HttpHeaders.authorizationHeader: 'Bearer ' + token,
|
||||
HttpHeaders.authorizationHeader: 'Bearer $token',
|
||||
HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8',
|
||||
HttpHeaders.acceptHeader: 'application/json'
|
||||
});
|
||||
|
|
|
@ -1,30 +1,43 @@
|
|||
/*
|
||||
Here are all the http requests to handle users
|
||||
*/
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:snipe_scanner/model/user.dart';
|
||||
|
||||
import '../main.dart';
|
||||
|
||||
// gets all users that match the optional search parameter
|
||||
Future<List<User>> getUsers(String token, [String search = ""]) async {
|
||||
final response = await http.get(
|
||||
Uri.parse("${service.get('host')}/api/v1/users?search=$search"),
|
||||
Uri.parse(
|
||||
"${service.get('host')}/api/v1/users?sort=name&order=asc&search=$search"),
|
||||
headers: {
|
||||
HttpHeaders.authorizationHeader: 'Bearer $token',
|
||||
HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8',
|
||||
});
|
||||
// snipe api definition does only specify 200 and 400 as response code, not sure what appens because it's not documented.
|
||||
if (response.statusCode != 200) {
|
||||
throw Error();
|
||||
}
|
||||
// parse the json
|
||||
final responseJson = jsonDecode(utf8.decode(response.bodyBytes));
|
||||
List<User> users =
|
||||
(responseJson["rows"] as List).map((i) => User.fromJson(i)).toList();
|
||||
users.sort((u, v) {
|
||||
return u.name.compareTo(v.name);
|
||||
});
|
||||
// create a list of users using the json response
|
||||
List<User> users = List.empty(growable: true);
|
||||
// try to catch errors while parsing the json, because snipe is stupid and regularly changes the responses on accident..
|
||||
try {
|
||||
users =
|
||||
(responseJson["rows"] as List).map((i) => User.fromJson(i)).toList();
|
||||
} catch (error, stackTrace) {
|
||||
Catcher2.reportCheckedError(error, stackTrace);
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
// get a single user using the user id
|
||||
Future<User> getUser(String token, int id) async {
|
||||
final response = await http
|
||||
.get(Uri.parse("${service.get('host')}/api/v1/users/$id"), headers: {
|
||||
|
@ -32,10 +45,23 @@ Future<User> getUser(String token, int id) async {
|
|||
HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8',
|
||||
HttpHeaders.acceptHeader: 'application/json',
|
||||
});
|
||||
if (response.statusCode != 200) {
|
||||
// other codes as 200 and 400 are not documented, however 404 is a user not found error, every other error should be reported to fix issues
|
||||
if (response.statusCode != 200 && response.statusCode != 404) {
|
||||
throw Error();
|
||||
}
|
||||
if (response.statusCode == 404) {
|
||||
throw Error();
|
||||
}
|
||||
final responseJson = jsonDecode(utf8.decode(response.bodyBytes));
|
||||
User user = User.fromJson(responseJson);
|
||||
return user;
|
||||
User? user;
|
||||
try {
|
||||
user = User.fromJson(responseJson);
|
||||
} catch (error, stackTrace) {
|
||||
Catcher2.reportCheckedError(error, stackTrace);
|
||||
}
|
||||
if (user != null) {
|
||||
return user;
|
||||
}
|
||||
// throw error otherwise.. we shouldn't arrive here, but this needs to be done to compile..
|
||||
throw Error();
|
||||
}
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import 'package:encrypt/encrypt.dart';
|
||||
|
||||
// helper class for the decryption of the api token within the scanner_users.json
|
||||
class ApiKeyEncryption {
|
||||
static String decryptKey(String encrypted, String pin) {
|
||||
final key = Key.fromUtf8(padPin(pin));
|
||||
final encrypter = Encrypter(AES(key, mode: AESMode.ecb));
|
||||
String decrypted = encrypter.decrypt(Encrypted.fromBase64(encrypted),
|
||||
iv: IV.fromLength(16));
|
||||
iv: IV.fromLength(
|
||||
16)); // this is so weak encryption that I hope noone will host this in the public web.
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
static String padPin(String pin) {
|
||||
while (pin.length < 32) {
|
||||
pin = '0' + pin;
|
||||
pin = '0$pin';
|
||||
}
|
||||
return pin;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
This file is responsible for playing the audio of successful and failed scans, it needs to be initialized once and offers two simple functions to play the corresponding sound.
|
||||
*/
|
||||
import 'dart:developer' as dev;
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:beep_player/beep_player.dart';
|
||||
|
||||
BeepFile _success = const BeepFile('assets/audios/success.wav');
|
||||
// this is a fun element of mattis saying allright, used in 1% of success beeps
|
||||
BeepFile _allright = const BeepFile('assets/audios/allright.wav');
|
||||
BeepFile _fail = const BeepFile('assets/audios/fail.wav');
|
||||
/*
|
||||
initialize the audio players by adding the sound files.
|
||||
*/
|
||||
void initAudio() async {
|
||||
BeepPlayer.load(_success);
|
||||
BeepPlayer.load(_allright);
|
||||
BeepPlayer.load(_fail);
|
||||
}
|
||||
|
||||
// Plays the success sound
|
||||
void playSuccess() {
|
||||
var rnd = Random().nextInt(1000);
|
||||
dev.log(rnd.toString());
|
||||
if (rnd <= 10) {
|
||||
BeepPlayer.play(_allright);
|
||||
return;
|
||||
}
|
||||
BeepPlayer.play(_success);
|
||||
}
|
||||
|
||||
// Plays the fail sound
|
||||
void playFail() {
|
||||
BeepPlayer.play(_fail);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
This file initializes the catcher2 logger. It sets the target for exception logging. For debugging purposes it logs simply to the console, in production mode it uses a provider so it can be seen remotely.
|
||||
*/
|
||||
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:sentry/sentry.dart';
|
||||
|
||||
initLogger(Widget? root, GlobalKey<NavigatorState> navigatorKey) {
|
||||
// options for the release version
|
||||
Catcher2Options releaseOptions = Catcher2Options(DialogReportMode(), [
|
||||
SentryHandler(SentryClient(SentryOptions(
|
||||
dsn:
|
||||
"https://7e8694aba1c0adcacd781c3a95baa8bc@o4506592201867264.ingest.sentry.io/4506592203636736")))
|
||||
]);
|
||||
// options in debug mode
|
||||
Catcher2Options debugOptions =
|
||||
Catcher2Options(DialogReportMode(), [ConsoleHandler()]);
|
||||
|
||||
Catcher2(
|
||||
rootWidget: root,
|
||||
debugConfig: debugOptions,
|
||||
releaseConfig: releaseOptions,
|
||||
navigatorKey:
|
||||
navigatorKey); // navigator key is needed for the dialog modal to be shown when an error occures, this allows the user to be asked if an error report should be submitted.
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:snipe_scanner/main.dart';
|
||||
|
||||
class RouteAwareWidget extends StatefulWidget {
|
||||
final String name;
|
||||
final Widget child;
|
||||
const RouteAwareWidget(this.name, {required this.child, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_RouteAwareWidgetState createState() => _RouteAwareWidgetState();
|
||||
}
|
||||
|
||||
class _RouteAwareWidgetState extends State<RouteAwareWidget> with RouteAware {
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
routeObserver.unsubscribe(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:snipe_scanner/main.dart';
|
||||
import 'package:snipe_scanner/utils/zebra_scan.dart';
|
||||
|
||||
// this class can be extended by view widgets to get the scanning functionality by overriding the onScan method. as it already has all the functionality to load data when a view gets added into the app you can do that aswell by overriding the loadData method
|
||||
abstract class ScanAwareState<T> extends State with RouteAware {
|
||||
void onScan(String barcode) {}
|
||||
void loadData() {}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
|
||||
}
|
||||
|
||||
// unsubscribe when the view gets disposed
|
||||
@override
|
||||
void dispose() {
|
||||
routeObserver.unsubscribe(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Bin zu Widget hin navigiert.
|
||||
@override
|
||||
void didPush() {
|
||||
loadData();
|
||||
ZebraScan.setScanHandler(onScan);
|
||||
super.didPush();
|
||||
}
|
||||
|
||||
// War bei Widget, hab zurück gedrückt
|
||||
@override
|
||||
void didPop() {
|
||||
ZebraScan.unsetScanHandler(onScan);
|
||||
super.didPop();
|
||||
}
|
||||
|
||||
// Bin zu Widget zurück gekommen.
|
||||
@override
|
||||
void didPopNext() {
|
||||
loadData();
|
||||
ZebraScan.setScanHandler(onScan);
|
||||
super.didPopNext();
|
||||
}
|
||||
|
||||
// Bin von Widget weg navigiert
|
||||
@override
|
||||
void didPushNext() {
|
||||
ZebraScan.unsetScanHandler(onScan);
|
||||
super.didPushNext();
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
// this is largely copied from the zebra datawedge demo https://github.com/ZebraDevs/DataWedge-Flutter-Demo/tree/master/datawedge_flutter
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:html_unescape/html_unescape.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -7,13 +8,13 @@ import 'package:snipe_scanner/model/activity.dart';
|
|||
import 'package:snipe_scanner/model/app_state.dart';
|
||||
import 'package:snipe_scanner/model/asset.dart';
|
||||
import 'package:snipe_scanner/service/asset.dart';
|
||||
import 'package:snipe_scanner/utils/zebra_scan.dart';
|
||||
import 'package:snipe_scanner/service/login_user.dart';
|
||||
import 'package:snipe_scanner/utils/audio.dart';
|
||||
import 'package:snipe_scanner/utils/scan_aware_state.dart';
|
||||
|
||||
import '../main.dart';
|
||||
|
||||
class ItemHistoryView extends StatelessWidget {
|
||||
class AssetHistoryView extends StatelessWidget {
|
||||
final List<Activity> history;
|
||||
const ItemHistoryView({Key? key, required this.history}) : super(key: key);
|
||||
const AssetHistoryView({super.key, required this.history});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -43,9 +44,9 @@ class ItemHistoryView extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class ItemGeneralView extends StatelessWidget {
|
||||
class AssetGeneralView extends StatelessWidget {
|
||||
final Asset asset;
|
||||
const ItemGeneralView({Key? key, required this.asset}) : super(key: key);
|
||||
const AssetGeneralView({super.key, required this.asset});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -69,10 +70,8 @@ class ItemGeneralView extends StatelessWidget {
|
|||
if (asset.assignedTo != null)
|
||||
TextRow(
|
||||
title: "Checked out to",
|
||||
text: asset.assignedTo!.name! +
|
||||
"\n(" +
|
||||
asset.lastCheckout!.formatted +
|
||||
")"),
|
||||
text:
|
||||
"${asset.assignedTo!.name!}\n(${asset.lastCheckout!.formatted})"),
|
||||
if (asset.assignedTo == null)
|
||||
const TextRow(title: "Checked out to", text: " - "),
|
||||
TextRow(
|
||||
|
@ -87,61 +86,26 @@ class ItemGeneralView extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class ItemDetail extends StatefulWidget {
|
||||
const ItemDetail({Key? key}) : super(key: key);
|
||||
class AssetDetail extends StatefulWidget {
|
||||
const AssetDetail({super.key});
|
||||
|
||||
@override
|
||||
_ItemDetailState createState() => _ItemDetailState();
|
||||
ScanAwareState<AssetDetail> createState() => _AssetDetailState();
|
||||
}
|
||||
|
||||
class _ItemDetailState extends State<ItemDetail> with RouteAware {
|
||||
class _AssetDetailState extends ScanAwareState<AssetDetail> {
|
||||
int _index = 0;
|
||||
String _error = "";
|
||||
bool _loaded = false;
|
||||
Asset? _asset;
|
||||
|
||||
void _onScan(barcode) {
|
||||
@override
|
||||
void onScan(barcode) {
|
||||
playSuccess();
|
||||
Navigator.pushReplacementNamed(context, "/assetDetail", arguments: barcode);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
void didPush() {
|
||||
ZebraScan.setScanHandler(_onScan);
|
||||
loadData();
|
||||
super.didPush();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
ZebraScan.setScanHandler(_onScan);
|
||||
loadData();
|
||||
super.didPopNext();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPop() {
|
||||
ZebraScan.unsetScanHandler(_onScan);
|
||||
super.didPop();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPushNext() {
|
||||
ZebraScan.unsetScanHandler(_onScan);
|
||||
super.didPushNext();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
routeObserver.unsubscribe(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void loadData() async {
|
||||
final args = ModalRoute.of(context)!.settings.arguments as String;
|
||||
var token = Provider.of<AppState>(context, listen: false).token;
|
||||
|
@ -153,14 +117,14 @@ class _ItemDetailState extends State<ItemDetail> with RouteAware {
|
|||
Asset asset;
|
||||
try {
|
||||
asset = await getAssetFromTag(token, args);
|
||||
} catch (err) {
|
||||
} on SnipeException catch (err) {
|
||||
setState(() {
|
||||
_error = err.toString();
|
||||
_loaded = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (asset.image != null) {
|
||||
if (asset.image != null && context.mounted) {
|
||||
await precacheImage(NetworkImage(asset.image!), context);
|
||||
}
|
||||
setState(() {
|
||||
|
@ -173,11 +137,11 @@ class _ItemDetailState extends State<ItemDetail> with RouteAware {
|
|||
if (_index == 1) {
|
||||
return FutureBuilder<List<Activity>>(
|
||||
future: getAssetActivity(
|
||||
Provider.of<AppState>(context, listen: false).token, _asset!.id!),
|
||||
Provider.of<AppState>(context, listen: false).token, _asset!.id),
|
||||
builder:
|
||||
(BuildContext context, AsyncSnapshot<List<Activity>> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return ItemHistoryView(history: snapshot.data!);
|
||||
return AssetHistoryView(history: snapshot.data!);
|
||||
} else if (snapshot.hasError) {
|
||||
return Container(
|
||||
padding:
|
||||
|
@ -192,7 +156,7 @@ class _ItemDetailState extends State<ItemDetail> with RouteAware {
|
|||
});
|
||||
}
|
||||
|
||||
return ItemGeneralView(asset: _asset!);
|
||||
return AssetGeneralView(asset: _asset!);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -223,7 +187,7 @@ class _ItemDetailState extends State<ItemDetail> with RouteAware {
|
|||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_asset!.name!),
|
||||
title: Text(_asset!.getShortName()),
|
||||
actions: [
|
||||
if (_asset!.availableActions!.checkout &&
|
||||
_asset!.assignedTo == null)
|
|
@ -1,71 +1,36 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:snipe_scanner/components/item_row.dart';
|
||||
import 'package:snipe_scanner/components/asset_row.dart';
|
||||
import 'package:snipe_scanner/components/location_selector.dart';
|
||||
import 'package:snipe_scanner/components/manual_input.dart';
|
||||
import 'package:snipe_scanner/model/app_state.dart';
|
||||
import 'package:snipe_scanner/model/asset.dart';
|
||||
import 'package:snipe_scanner/service/asset.dart';
|
||||
import 'package:snipe_scanner/utils/zebra_scan.dart';
|
||||
|
||||
import '../main.dart';
|
||||
import 'package:snipe_scanner/utils/audio.dart';
|
||||
import 'package:snipe_scanner/utils/scan_aware_state.dart';
|
||||
|
||||
class Audit extends StatefulWidget {
|
||||
const Audit({Key? key}) : super(key: key);
|
||||
const Audit({super.key});
|
||||
|
||||
@override
|
||||
_AuditState createState() => _AuditState();
|
||||
ScanAwareState<Audit> createState() => _AuditState();
|
||||
}
|
||||
|
||||
class _AuditState extends State<Audit> with RouteAware {
|
||||
class _AuditState extends ScanAwareState<Audit> {
|
||||
final List<dynamic> _history = [];
|
||||
final TextEditingController _noteController = TextEditingController();
|
||||
|
||||
int _dst = -1;
|
||||
bool _keepNote = false;
|
||||
|
||||
// Bin zu Dashboard hin navigiert.
|
||||
@override
|
||||
void didPush() {
|
||||
ZebraScan.setScanHandler(_auditAsset);
|
||||
super.didPush();
|
||||
}
|
||||
|
||||
// War bei Dashboard, hab zurück gedrückt
|
||||
@override
|
||||
void didPop() {
|
||||
ZebraScan.unsetScanHandler(_auditAsset);
|
||||
super.didPop();
|
||||
}
|
||||
|
||||
// Bin zu Dashboard zurück gekommen.
|
||||
@override
|
||||
void didPopNext() {
|
||||
ZebraScan.setScanHandler(_auditAsset);
|
||||
super.didPopNext();
|
||||
}
|
||||
|
||||
// Bin von Dashboard weg navigiert
|
||||
@override
|
||||
void didPushNext() {
|
||||
ZebraScan.unsetScanHandler(_auditAsset);
|
||||
super.didPushNext();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_noteController.dispose();
|
||||
routeObserver.unsubscribe(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _auditAsset(String barcode) async {
|
||||
@override
|
||||
void onScan(String barcode) async {
|
||||
var token = Provider.of<AppState>(context, listen: false).token;
|
||||
Map<String, dynamic> item = {"barcode": barcode};
|
||||
setState(() {
|
||||
|
@ -75,16 +40,16 @@ class _AuditState extends State<Audit> with RouteAware {
|
|||
try {
|
||||
asset = await getAssetFromTag(token, barcode);
|
||||
} catch (err) {
|
||||
playFail();
|
||||
setState(() {
|
||||
item["status"] = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (asset.name != null) {
|
||||
setState(() {
|
||||
item["name"] = asset.name;
|
||||
});
|
||||
}
|
||||
playSuccess();
|
||||
setState(() {
|
||||
item["name"] = asset.getShortName();
|
||||
});
|
||||
try {
|
||||
await auditAsset(token, asset.assetTag!,
|
||||
note: _noteController.text, dstId: _dst);
|
||||
|
@ -114,17 +79,19 @@ class _AuditState extends State<Audit> with RouteAware {
|
|||
final result =
|
||||
await Navigator.pushNamed(context, '/searchAsset');
|
||||
if (result != null) {
|
||||
_auditAsset('$result');
|
||||
onScan('$result');
|
||||
}
|
||||
}),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
LocationSelector(cb: (id) {
|
||||
_dst = id;
|
||||
}),
|
||||
ManualInput(cb: _auditAsset),
|
||||
LocationSelector(
|
||||
instant: true,
|
||||
cb: (id) {
|
||||
_dst = id;
|
||||
}),
|
||||
ManualInput(cb: onScan),
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: TextField(
|
||||
|
@ -132,7 +99,7 @@ class _AuditState extends State<Audit> with RouteAware {
|
|||
controller: _noteController),
|
||||
),
|
||||
SizedBox(
|
||||
height: 50,
|
||||
height: 34,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
|
@ -152,7 +119,7 @@ class _AuditState extends State<Audit> with RouteAware {
|
|||
],
|
||||
)),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 4),
|
||||
child: const Text(
|
||||
"History",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 24),
|
||||
|
@ -164,7 +131,7 @@ class _AuditState extends State<Audit> with RouteAware {
|
|||
itemCount: _history.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var i = _history.length - (index + 1);
|
||||
return itemRow(_history[i]["barcode"], context,
|
||||
return assetRow(_history[i]["barcode"], context,
|
||||
name: _history[i]["name"],
|
||||
note: _history[i]["note"],
|
||||
status: _history[i]["status"]);
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:snipe_scanner/components/item_row.dart';
|
||||
import 'package:snipe_scanner/components/asset_row.dart';
|
||||
import 'package:snipe_scanner/components/manual_input.dart';
|
||||
import 'package:snipe_scanner/model/app_state.dart';
|
||||
import 'package:snipe_scanner/model/asset.dart';
|
||||
import 'package:snipe_scanner/service/asset.dart';
|
||||
import 'package:snipe_scanner/utils/zebra_scan.dart';
|
||||
import 'package:snipe_scanner/utils/audio.dart';
|
||||
import 'package:snipe_scanner/utils/scan_aware_state.dart';
|
||||
|
||||
import '../main.dart';
|
||||
|
||||
class BulkCheckin extends StatefulWidget {
|
||||
const BulkCheckin({Key? key}) : super(key: key);
|
||||
const BulkCheckin({super.key});
|
||||
|
||||
@override
|
||||
_BulkCheckinState createState() => _BulkCheckinState();
|
||||
ScanAwareState<BulkCheckin> createState() => _BulkCheckinState();
|
||||
}
|
||||
|
||||
class _BulkCheckinState extends State<BulkCheckin> with RouteAware {
|
||||
class _BulkCheckinState extends ScanAwareState<BulkCheckin> {
|
||||
final List<dynamic> _history = [];
|
||||
final TextEditingController _noteController = TextEditingController();
|
||||
bool _keepNote = false;
|
||||
|
@ -27,44 +28,9 @@ class _BulkCheckinState extends State<BulkCheckin> with RouteAware {
|
|||
Asset? _checkinAlertAsset;
|
||||
Map<String, dynamic>? _checkinAlertItem;
|
||||
|
||||
// Bin zu Dashboard hin navigiert.
|
||||
@override
|
||||
void didPush() {
|
||||
ZebraScan.setScanHandler(_checkinAsset);
|
||||
super.didPush();
|
||||
}
|
||||
|
||||
// War bei Dashboard, hab zurück gedrückt
|
||||
@override
|
||||
void didPop() {
|
||||
ZebraScan.unsetScanHandler(_checkinAsset);
|
||||
super.didPop();
|
||||
}
|
||||
|
||||
// Bin zu Dashboard zurück gekommen.
|
||||
@override
|
||||
void didPopNext() {
|
||||
ZebraScan.setScanHandler(_checkinAsset);
|
||||
super.didPopNext();
|
||||
}
|
||||
|
||||
// Bin von Dashboard weg navigiert
|
||||
@override
|
||||
void didPushNext() {
|
||||
ZebraScan.unsetScanHandler(_checkinAsset);
|
||||
super.didPushNext();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_noteController.dispose();
|
||||
routeObserver.unsubscribe(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -91,6 +57,11 @@ class _BulkCheckinState extends State<BulkCheckin> with RouteAware {
|
|||
_checkinFunc(token, asset, item);
|
||||
}
|
||||
|
||||
@override
|
||||
void onScan(String barcode) {
|
||||
_checkinAsset(barcode);
|
||||
}
|
||||
|
||||
void _checkinAsset(String barcode) async {
|
||||
_cancelCheckin();
|
||||
setState(() {
|
||||
|
@ -109,6 +80,7 @@ class _BulkCheckinState extends State<BulkCheckin> with RouteAware {
|
|||
_error = "Asset not found";
|
||||
item["status"] = false;
|
||||
});
|
||||
playFail();
|
||||
return;
|
||||
}
|
||||
item["id"] = asset.id;
|
||||
|
@ -117,7 +89,7 @@ class _BulkCheckinState extends State<BulkCheckin> with RouteAware {
|
|||
if (asset.assignedTo != null) {
|
||||
item["checkinFrom"] = asset.assignedTo!.name;
|
||||
}
|
||||
item["name"] = asset.name;
|
||||
item["name"] = asset.getShortName();
|
||||
});
|
||||
}
|
||||
if (asset.assignedTo != null &&
|
||||
|
@ -141,14 +113,16 @@ class _BulkCheckinState extends State<BulkCheckin> with RouteAware {
|
|||
void _checkinFunc(
|
||||
String token, Asset asset, Map<String, dynamic> item) async {
|
||||
try {
|
||||
await checkinAsset(token, asset.id!, note: _noteController.text);
|
||||
await checkinAsset(token, asset.id, note: _noteController.text);
|
||||
} catch (err) {
|
||||
setState(() {
|
||||
_error = err.toString();
|
||||
item["status"] = false;
|
||||
});
|
||||
playFail();
|
||||
return;
|
||||
}
|
||||
playSuccess();
|
||||
setState(() {
|
||||
item["note"] = _noteController.text;
|
||||
item["status"] = true;
|
||||
|
@ -217,7 +191,7 @@ class _BulkCheckinState extends State<BulkCheckin> with RouteAware {
|
|||
itemCount: _history.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var i = _history.length - (index + 1);
|
||||
return itemRow(_history[i]["barcode"], context,
|
||||
return assetRow(_history[i]["barcode"], context,
|
||||
name: _history[i]["name"],
|
||||
note: _history[i]["note"],
|
||||
status: _history[i]["status"],
|
||||
|
@ -233,6 +207,7 @@ class _BulkCheckinState extends State<BulkCheckin> with RouteAware {
|
|||
'Do you really want to check in this asset from another asset? Maybe it belongs to a set.'),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Checkbox(
|
||||
onChanged: (bool? value) {
|
||||
|
@ -245,7 +220,6 @@ class _BulkCheckinState extends State<BulkCheckin> with RouteAware {
|
|||
),
|
||||
const Text('Don\'t ask again'),
|
||||
],
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:snipe_scanner/components/item_row.dart';
|
||||
import 'package:snipe_scanner/components/asset_row.dart';
|
||||
import 'package:snipe_scanner/components/manual_input.dart';
|
||||
import 'package:snipe_scanner/components/target_selector.dart';
|
||||
import 'package:snipe_scanner/model/app_state.dart';
|
||||
import 'package:snipe_scanner/model/asset.dart';
|
||||
import 'package:snipe_scanner/service/asset.dart';
|
||||
import 'package:snipe_scanner/utils/zebra_scan.dart';
|
||||
|
||||
import '../main.dart';
|
||||
import 'package:snipe_scanner/utils/audio.dart';
|
||||
import 'package:snipe_scanner/utils/scan_aware_state.dart';
|
||||
|
||||
class BulkCheckout extends StatefulWidget {
|
||||
const BulkCheckout({Key? key}) : super(key: key);
|
||||
const BulkCheckout({super.key});
|
||||
|
||||
@override
|
||||
_BulkCheckoutState createState() => _BulkCheckoutState();
|
||||
ScanAwareState<BulkCheckout> createState() => _BulkCheckoutState();
|
||||
}
|
||||
|
||||
class _BulkCheckoutState extends State<BulkCheckout> with RouteAware {
|
||||
class _BulkCheckoutState extends ScanAwareState<BulkCheckout> {
|
||||
final List<dynamic> _history = [];
|
||||
final TextEditingController _noteController = TextEditingController();
|
||||
|
||||
|
@ -27,48 +25,14 @@ class _BulkCheckoutState extends State<BulkCheckout> with RouteAware {
|
|||
bool _keepNote = false;
|
||||
String _error = "";
|
||||
|
||||
// Bin zu Dashboard hin navigiert.
|
||||
@override
|
||||
void didPush() {
|
||||
ZebraScan.setScanHandler(_checkoutAsset);
|
||||
super.didPush();
|
||||
}
|
||||
|
||||
// War bei Dashboard, hab zurück gedrückt
|
||||
@override
|
||||
void didPop() {
|
||||
ZebraScan.unsetScanHandler(_checkoutAsset);
|
||||
super.didPop();
|
||||
}
|
||||
|
||||
// Bin zu Dashboard zurück gekommen.
|
||||
@override
|
||||
void didPopNext() {
|
||||
ZebraScan.setScanHandler(_checkoutAsset);
|
||||
super.didPopNext();
|
||||
}
|
||||
|
||||
// Bin von Dashboard weg navigiert
|
||||
@override
|
||||
void didPushNext() {
|
||||
ZebraScan.unsetScanHandler(_checkoutAsset);
|
||||
super.didPushNext();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_noteController.dispose();
|
||||
routeObserver.unsubscribe(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _checkoutAsset(String barcode) async {
|
||||
@override
|
||||
void onScan(String barcode) async {
|
||||
setState(() {
|
||||
_error = "";
|
||||
});
|
||||
|
@ -85,6 +49,7 @@ class _BulkCheckoutState extends State<BulkCheckout> with RouteAware {
|
|||
_error = "Asset not found";
|
||||
item["status"] = false;
|
||||
});
|
||||
playFail();
|
||||
return;
|
||||
}
|
||||
if (asset.name != null) {
|
||||
|
@ -92,19 +57,21 @@ class _BulkCheckoutState extends State<BulkCheckout> with RouteAware {
|
|||
if (asset.assignedTo != null) {
|
||||
item["checkoutFrom"] = asset.assignedTo!.name;
|
||||
}
|
||||
item["name"] = asset.name;
|
||||
item["name"] = asset.getShortName();
|
||||
});
|
||||
}
|
||||
try {
|
||||
await checkoutAsset(token, asset.id!, _dst,
|
||||
await checkoutAsset(token, asset.id, _dst,
|
||||
note: _noteController.text, type: _type);
|
||||
} catch (err) {
|
||||
setState(() {
|
||||
_error = err.toString();
|
||||
item["status"] = false;
|
||||
});
|
||||
playFail();
|
||||
return;
|
||||
}
|
||||
playSuccess();
|
||||
setState(() {
|
||||
item["note"] = _noteController.text;
|
||||
item["status"] = true;
|
||||
|
@ -125,7 +92,7 @@ class _BulkCheckoutState extends State<BulkCheckout> with RouteAware {
|
|||
final result =
|
||||
await Navigator.pushNamed(context, '/searchAsset');
|
||||
if (result != null) {
|
||||
_checkoutAsset('$result');
|
||||
onScan('$result');
|
||||
}
|
||||
}),
|
||||
],
|
||||
|
@ -137,7 +104,7 @@ class _BulkCheckoutState extends State<BulkCheckout> with RouteAware {
|
|||
_dst = id;
|
||||
_type = type;
|
||||
}),
|
||||
ManualInput(cb: _checkoutAsset),
|
||||
ManualInput(cb: onScan),
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: TextField(
|
||||
|
@ -145,7 +112,7 @@ class _BulkCheckoutState extends State<BulkCheckout> with RouteAware {
|
|||
controller: _noteController),
|
||||
),
|
||||
SizedBox(
|
||||
height: 50,
|
||||
height: 34,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
|
@ -165,7 +132,7 @@ class _BulkCheckoutState extends State<BulkCheckout> with RouteAware {
|
|||
],
|
||||
)),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, 4),
|
||||
child: const Text(
|
||||
"History",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 24),
|
||||
|
@ -177,7 +144,7 @@ class _BulkCheckoutState extends State<BulkCheckout> with RouteAware {
|
|||
itemCount: _history.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var i = _history.length - (index + 1);
|
||||
return itemRow(_history[i]["barcode"], context,
|
||||
return assetRow(_history[i]["barcode"], context,
|
||||
name: _history[i]["name"],
|
||||
note: _history[i]["note"],
|
||||
status: _history[i]["status"]);
|
||||
|
|
|
@ -6,10 +6,10 @@ import 'package:snipe_scanner/model/asset.dart';
|
|||
import 'package:snipe_scanner/service/asset.dart';
|
||||
|
||||
class CheckinAsset extends StatefulWidget {
|
||||
const CheckinAsset({Key? key}) : super(key: key);
|
||||
const CheckinAsset({super.key});
|
||||
|
||||
@override
|
||||
_CheckinAssetState createState() => _CheckinAssetState();
|
||||
State<CheckinAsset> createState() => _CheckinAssetState();
|
||||
}
|
||||
|
||||
class _CheckinAssetState extends State<CheckinAsset> {
|
||||
|
@ -29,7 +29,7 @@ class _CheckinAssetState extends State<CheckinAsset> {
|
|||
_asset = ModalRoute.of(context)!.settings.arguments as Asset;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text("Checkin " + _asset!.name!)),
|
||||
appBar: AppBar(title: Text("Checkin ${_asset!.getShortName()}")),
|
||||
body: Column(
|
||||
children: [
|
||||
Container(
|
||||
|
@ -53,7 +53,7 @@ class _CheckinAssetState extends State<CheckinAsset> {
|
|||
await checkinAsset(
|
||||
Provider.of<AppState>(context, listen: false)
|
||||
.token,
|
||||
_asset!.id!,
|
||||
_asset!.id,
|
||||
note: _noteController.text);
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
|
@ -65,7 +65,7 @@ class _CheckinAssetState extends State<CheckinAsset> {
|
|||
setState(() {
|
||||
_buttonEnabled = true;
|
||||
});
|
||||
Navigator.pop(context);
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
}
|
||||
: null),
|
||||
if (!_buttonEnabled) const CircularProgressIndicator(),
|
||||
|
|
|
@ -7,10 +7,10 @@ import 'package:snipe_scanner/model/asset.dart';
|
|||
import 'package:snipe_scanner/service/asset.dart';
|
||||
|
||||
class CheckoutAsset extends StatefulWidget {
|
||||
const CheckoutAsset({Key? key}) : super(key: key);
|
||||
const CheckoutAsset({super.key});
|
||||
|
||||
@override
|
||||
_CheckoutAssetState createState() => _CheckoutAssetState();
|
||||
State<CheckoutAsset> createState() => _CheckoutAssetState();
|
||||
}
|
||||
|
||||
class _CheckoutAssetState extends State<CheckoutAsset> {
|
||||
|
@ -32,7 +32,7 @@ class _CheckoutAssetState extends State<CheckoutAsset> {
|
|||
_asset = ModalRoute.of(context)!.settings.arguments as Asset;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text("Checkout " + _asset!.name!)),
|
||||
appBar: AppBar(title: Text("Checkout ${_asset!.getShortName()}")),
|
||||
body: Column(
|
||||
children: [
|
||||
TargetSelector(
|
||||
|
@ -61,7 +61,7 @@ class _CheckoutAssetState extends State<CheckoutAsset> {
|
|||
await checkoutAsset(
|
||||
Provider.of<AppState>(context, listen: false)
|
||||
.token,
|
||||
_asset!.id!,
|
||||
_asset!.id,
|
||||
_dst,
|
||||
type: _type,
|
||||
note: _noteController.text);
|
||||
|
@ -75,7 +75,7 @@ class _CheckoutAssetState extends State<CheckoutAsset> {
|
|||
setState(() {
|
||||
_buttonEnabled = true;
|
||||
});
|
||||
Navigator.pop(context);
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
}
|
||||
: null),
|
||||
if (!_buttonEnabled) const CircularProgressIndicator(),
|
||||
|
|
|
@ -1,47 +1,37 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:snipe_scanner/components/big_button.dart';
|
||||
import 'package:snipe_scanner/components/build_info.dart';
|
||||
import 'package:snipe_scanner/components/manual_input.dart';
|
||||
import 'package:snipe_scanner/components/user_selector.dart';
|
||||
import 'package:snipe_scanner/main.dart';
|
||||
import 'package:snipe_scanner/model/app_state.dart';
|
||||
import 'package:snipe_scanner/service/login_user.dart';
|
||||
import 'package:snipe_scanner/utils/zebra_scan.dart';
|
||||
import 'package:snipe_scanner/utils/audio.dart';
|
||||
import 'package:snipe_scanner/utils/scan_aware_state.dart';
|
||||
|
||||
class DashboardWidget extends StatefulWidget {
|
||||
const DashboardWidget({Key? key}) : super(key: key);
|
||||
const DashboardWidget({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => DashboardWidgetState();
|
||||
ScanAwareState<DashboardWidget> createState() => _DashboardWidgetState();
|
||||
}
|
||||
|
||||
class DashboardWidgetState extends State<DashboardWidget> with RouteAware {
|
||||
class _DashboardWidgetState extends ScanAwareState<DashboardWidget> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
String _username = "";
|
||||
String _avatar = "";
|
||||
bool loaded = false;
|
||||
|
||||
void _onScan(barcode) {
|
||||
Navigator.pushNamed(context, "/assetDetail", arguments: barcode);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
loadData();
|
||||
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
|
||||
log("DID CHANGE DEPENDENCIES");
|
||||
void onScan(String barcode) {
|
||||
playSuccess();
|
||||
Navigator.pushNamed(context, "/assetDetail", arguments: barcode);
|
||||
}
|
||||
|
||||
// Bin zu Dashboard hin navigiert.
|
||||
@override
|
||||
void didPush() {
|
||||
log("DID PUSH!");
|
||||
FocusScope.of(context).unfocus();
|
||||
ZebraScan.setScanHandler(_onScan);
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.animateTo(0,
|
||||
duration: const Duration(milliseconds: 500), curve: Curves.easeIn);
|
||||
|
@ -49,18 +39,10 @@ class DashboardWidgetState extends State<DashboardWidget> with RouteAware {
|
|||
super.didPush();
|
||||
}
|
||||
|
||||
// War bei Dashboard, hab zurück gedrückt
|
||||
@override
|
||||
void didPop() {
|
||||
ZebraScan.unsetScanHandler(_onScan);
|
||||
super.didPop();
|
||||
}
|
||||
|
||||
// Bin zu Dashboard zurück gekommen.
|
||||
@override
|
||||
void didPopNext() {
|
||||
FocusScope.of(context).unfocus();
|
||||
ZebraScan.setScanHandler(_onScan);
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.animateTo(0,
|
||||
duration: const Duration(milliseconds: 500), curve: Curves.easeIn);
|
||||
|
@ -68,35 +50,25 @@ class DashboardWidgetState extends State<DashboardWidget> with RouteAware {
|
|||
super.didPopNext();
|
||||
}
|
||||
|
||||
// Bin von Dashboard weg navigiert
|
||||
@override
|
||||
void didPushNext() {
|
||||
ZebraScan.unsetScanHandler(_onScan);
|
||||
super.didPushNext();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
routeObserver.unsubscribe(this);
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
void loadData() async {
|
||||
var token = Provider.of<AppState>(context).token;
|
||||
var token = Provider.of<AppState>(context, listen: false).token;
|
||||
var user = await getUser(token);
|
||||
var avatar = "";
|
||||
if (user.avatar.startsWith("//")) {
|
||||
avatar = "https:" + user.avatar;
|
||||
avatar = "https:${user.avatar}";
|
||||
} else {
|
||||
avatar = user.avatar;
|
||||
}
|
||||
await precacheImage(NetworkImage(avatar), context);
|
||||
if (context.mounted) {
|
||||
await precacheImage(NetworkImage(avatar), context);
|
||||
}
|
||||
setState(() {
|
||||
_username = user.name;
|
||||
_avatar = avatar;
|
||||
|
@ -120,6 +92,7 @@ class DashboardWidgetState extends State<DashboardWidget> with RouteAware {
|
|||
children: [
|
||||
SizedBox(height: size.height * 0.05),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
|
@ -139,7 +112,6 @@ class DashboardWidgetState extends State<DashboardWidget> with RouteAware {
|
|||
child: const BuildInfo(),
|
||||
)
|
||||
],
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
|
@ -167,18 +139,19 @@ class DashboardWidgetState extends State<DashboardWidget> with RouteAware {
|
|||
],
|
||||
)),
|
||||
SizedBox(height: size.height * 0.02),
|
||||
ManualInput(cb: _onScan),
|
||||
bigButton("bulk checkin",
|
||||
ManualInput(cb: onScan),
|
||||
bigButton("Bulk checkin",
|
||||
() => Navigator.pushNamed(context, "/bulkCheckin")),
|
||||
bigButton("bulk checkout",
|
||||
bigButton("Bulk checkout",
|
||||
() => Navigator.pushNamed(context, '/bulkCheckout')),
|
||||
bigButton(
|
||||
"audit", () => Navigator.pushNamed(context, '/auditAsset')),
|
||||
bigButton("search asset", () async {
|
||||
"Audit", () => Navigator.pushNamed(context, '/auditAsset')),
|
||||
bigButton("Search asset", () async {
|
||||
final result =
|
||||
await Navigator.pushNamed(context, '/searchAsset');
|
||||
if (!mounted) return;
|
||||
if (result != null) {
|
||||
_onScan(result);
|
||||
onScan("$result");
|
||||
}
|
||||
}),
|
||||
UserSelector(
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:snipe_scanner/components/build_info.dart';
|
||||
import 'package:snipe_scanner/main.dart';
|
||||
import 'package:snipe_scanner/model/app_state.dart';
|
||||
import 'package:snipe_scanner/model/login_user.dart';
|
||||
import 'package:snipe_scanner/service/login_user.dart';
|
||||
import 'package:snipe_scanner/utils/api_key_encryption.dart';
|
||||
import 'package:snipe_scanner/utils/zebra_scan.dart';
|
||||
import 'package:snipe_scanner/utils/audio.dart';
|
||||
import 'package:snipe_scanner/utils/scan_aware_state.dart';
|
||||
import 'package:snipe_scanner/widgets/preferences.dart';
|
||||
|
||||
import '../main.dart';
|
||||
|
||||
class LoginWidget extends StatefulWidget {
|
||||
const LoginWidget({Key? key}) : super(key: key);
|
||||
const LoginWidget({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => LoginWidgetState();
|
||||
}
|
||||
|
||||
class LoginWidgetState extends State<LoginWidget> with RouteAware {
|
||||
class LoginWidgetState extends ScanAwareState<LoginWidget> {
|
||||
String dropdownValue = 'Choose...';
|
||||
final List<String> _users = ['Choose...'];
|
||||
List<LoginUser> users = [];
|
||||
|
@ -31,7 +33,6 @@ class LoginWidgetState extends State<LoginWidget> with RouteAware {
|
|||
void initState() {
|
||||
_pinController = TextEditingController();
|
||||
_pinController.clear();
|
||||
log("init");
|
||||
getUsers();
|
||||
super.initState();
|
||||
}
|
||||
|
@ -39,17 +40,12 @@ class LoginWidgetState extends State<LoginWidget> with RouteAware {
|
|||
@override
|
||||
void dispose() {
|
||||
_pinController.dispose();
|
||||
routeObserver.unsubscribe(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
|
||||
}
|
||||
|
||||
void _onScan(String barcode) {
|
||||
void onScan(String barcode) {
|
||||
playSuccess();
|
||||
if (barcode != "") {
|
||||
_login(barcode);
|
||||
}
|
||||
|
@ -64,45 +60,42 @@ class LoginWidgetState extends State<LoginWidget> with RouteAware {
|
|||
}
|
||||
|
||||
@override
|
||||
void didPush() {
|
||||
resetState();
|
||||
void loadData() {
|
||||
getUsers();
|
||||
ZebraScan.setScanHandler(_onScan);
|
||||
super.didPush();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPop() {
|
||||
resetState();
|
||||
ZebraScan.unsetScanHandler(_onScan);
|
||||
super.didPop();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
resetState();
|
||||
getUsers();
|
||||
ZebraScan.setScanHandler(_onScan);
|
||||
super.didPopNext();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPushNext() {
|
||||
resetState();
|
||||
ZebraScan.unsetScanHandler(_onScan);
|
||||
super.didPushNext();
|
||||
}
|
||||
|
||||
void getUsers() async {
|
||||
if (service.get('host') == "") {
|
||||
setState(() {
|
||||
loading = false;
|
||||
_errorText =
|
||||
"You need to set the Address to your SnipeIT instance in the settings.";
|
||||
});
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
loading = true;
|
||||
_errorText = "";
|
||||
});
|
||||
try {
|
||||
users = await getLoginUsers();
|
||||
} catch (err) {
|
||||
} on ClientException catch (err) {
|
||||
if (err.message == "Connection refused") {
|
||||
setState(() {
|
||||
_errorText = "The connection to get the user list was refused.";
|
||||
loading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
rethrow;
|
||||
} on HttpException catch (err) {
|
||||
var errorText = "An unexpected error occured.";
|
||||
if (err.code == 404) {
|
||||
errorText = "Userlist was not found.";
|
||||
}
|
||||
setState(() {
|
||||
_errorText = "Could not get Userlist.";
|
||||
_errorText = errorText;
|
||||
loading = false;
|
||||
});
|
||||
return;
|
||||
|
@ -122,12 +115,12 @@ class LoginWidgetState extends State<LoginWidget> with RouteAware {
|
|||
try {
|
||||
await getUser(token);
|
||||
} on HttpException catch (err) {
|
||||
var message = "Ein Fehler ist aufgetreten: " + err.toString();
|
||||
var message = "Ein Fehler ist aufgetreten: $err";
|
||||
if (err.code == 401) {
|
||||
message = "Ungültiger Token";
|
||||
}
|
||||
setState(() {
|
||||
_errorText = message + " " + err.body;
|
||||
_errorText = "$message ${err.body}";
|
||||
});
|
||||
return;
|
||||
} catch (err) {
|
||||
|
@ -136,7 +129,7 @@ class LoginWidgetState extends State<LoginWidget> with RouteAware {
|
|||
});
|
||||
return;
|
||||
}
|
||||
Navigator.pushNamed(context, "/dashboard");
|
||||
if (context.mounted) Navigator.pushNamed(context, "/dashboard");
|
||||
}
|
||||
|
||||
void onEnter(string) {
|
||||
|
@ -177,6 +170,7 @@ class LoginWidgetState extends State<LoginWidget> with RouteAware {
|
|||
children: [
|
||||
SizedBox(height: size.height * 0.05),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
|
@ -185,10 +179,7 @@ class LoginWidgetState extends State<LoginWidget> with RouteAware {
|
|||
),
|
||||
padding: const EdgeInsets.all(0),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const PreferencesWidget()));
|
||||
Navigator.pushNamed(context, '/preferences');
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
|
@ -197,7 +188,6 @@ class LoginWidgetState extends State<LoginWidget> with RouteAware {
|
|||
child: const BuildInfo(),
|
||||
)
|
||||
],
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
),
|
||||
SizedBox(height: size.height * 0.07),
|
||||
Container(
|
||||
|
@ -310,8 +300,8 @@ class LoginWidgetState extends State<LoginWidget> with RouteAware {
|
|||
),
|
||||
),
|
||||
Container(
|
||||
child: Divider(height: size.height * 0.16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 80),
|
||||
child: Divider(height: size.height * 0.16),
|
||||
),
|
||||
const Text("Or Scan QR Code to Login")
|
||||
],
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:pref/pref.dart';
|
||||
|
||||
class PreferencesWidget extends StatefulWidget {
|
||||
const PreferencesWidget({Key? key}) : super(key: key);
|
||||
const PreferencesWidget({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => PreferencesWidgetState();
|
||||
|
@ -15,10 +15,19 @@ class PreferencesWidgetState extends State<PreferencesWidget> {
|
|||
appBar: AppBar(title: const Text('Settings')),
|
||||
body: PrefPage(
|
||||
children: [
|
||||
const PrefText(
|
||||
PrefText(
|
||||
pref: 'host',
|
||||
label: 'Host',
|
||||
keyboardType: TextInputType.url,
|
||||
validator: (str) {
|
||||
if (str == null || str == "") {
|
||||
return 'Host is required';
|
||||
}
|
||||
if (!str.startsWith("http://") && !str.startsWith("https://")) {
|
||||
return 'This needs to include http:// or https://';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
PrefCheckbox(
|
||||
pref: 'use_custom_user_url',
|
||||
|
@ -29,13 +38,23 @@ class PreferencesWidgetState extends State<PreferencesWidget> {
|
|||
}
|
||||
},
|
||||
),
|
||||
const PrefHider(children: [
|
||||
PrefHider(pref: 'use_custom_user_url', children: [
|
||||
PrefText(
|
||||
pref: 'custom_user_url',
|
||||
label: 'Custom user JSON url',
|
||||
keyboardType: TextInputType.url,
|
||||
validator: (str) {
|
||||
if (str == null || str == "") {
|
||||
return 'Custom user URL is required';
|
||||
}
|
||||
if (!str.startsWith("http://") &&
|
||||
!str.startsWith("https://")) {
|
||||
return 'This needs to include http:// or https://';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
], pref: 'use_custom_user_url'),
|
||||
]),
|
||||
const PrefText(pref: 'tag_prefix', label: 'Tag Prefix'),
|
||||
PrefCheckbox(
|
||||
pref: 'auto_logout',
|
||||
|
@ -50,7 +69,7 @@ class PreferencesWidgetState extends State<PreferencesWidget> {
|
|||
pref: 'warn_before_checkin_from_asset',
|
||||
title: Text("Warn before checking in an asset from an asset"),
|
||||
),
|
||||
PrefHider(children: [
|
||||
PrefHider(pref: 'auto_logout', children: [
|
||||
PrefSlider(
|
||||
pref: 'auto_logout_time',
|
||||
title: const Text('Auto logout timeout'),
|
||||
|
@ -58,7 +77,7 @@ class PreferencesWidgetState extends State<PreferencesWidget> {
|
|||
max: 300,
|
||||
trailing: (num v) => Text('$v minutes'),
|
||||
),
|
||||
], pref: 'auto_logout')
|
||||
])
|
||||
],
|
||||
));
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:snipe_scanner/components/search_item_row.dart';
|
||||
|
@ -6,10 +7,10 @@ import 'package:snipe_scanner/model/asset.dart';
|
|||
import 'package:snipe_scanner/service/asset.dart';
|
||||
|
||||
class SearchAsset extends StatefulWidget {
|
||||
const SearchAsset({Key? key}) : super(key: key);
|
||||
const SearchAsset({super.key});
|
||||
|
||||
@override
|
||||
_SearchAssetState createState() => _SearchAssetState();
|
||||
State<SearchAsset> createState() => _SearchAssetState();
|
||||
}
|
||||
|
||||
class _SearchAssetState extends State<SearchAsset> {
|
||||
|
@ -31,10 +32,11 @@ class _SearchAssetState extends State<SearchAsset> {
|
|||
try {
|
||||
assets = await getAssets(
|
||||
Provider.of<AppState>(context, listen: false).token, value);
|
||||
} catch (e) {
|
||||
} catch (err, stackTrace) {
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_error = err.toString();
|
||||
});
|
||||
Catcher2.reportCheckedError(err, stackTrace);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
|
|
|
@ -7,12 +7,11 @@ import 'package:snipe_scanner/model/asset.dart';
|
|||
import 'package:snipe_scanner/model/user.dart';
|
||||
import 'package:snipe_scanner/service/asset.dart';
|
||||
import 'package:snipe_scanner/service/users.dart';
|
||||
|
||||
import '../main.dart';
|
||||
import 'package:snipe_scanner/utils/scan_aware_state.dart';
|
||||
|
||||
class UserGeneralView extends StatelessWidget {
|
||||
final User user;
|
||||
const UserGeneralView({Key? key, required this.user}) : super(key: key);
|
||||
const UserGeneralView({super.key, required this.user});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -50,7 +49,7 @@ class UserGeneralView extends StatelessWidget {
|
|||
|
||||
class UserAssetsView extends StatelessWidget {
|
||||
final List<Asset> assets;
|
||||
const UserAssetsView({Key? key, required this.assets}) : super(key: key);
|
||||
const UserAssetsView({super.key, required this.assets});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -84,42 +83,19 @@ class UserAssetsView extends StatelessWidget {
|
|||
}
|
||||
|
||||
class UserDetail extends StatefulWidget {
|
||||
const UserDetail({Key? key}) : super(key: key);
|
||||
const UserDetail({super.key});
|
||||
|
||||
@override
|
||||
_UserDetailState createState() => _UserDetailState();
|
||||
ScanAwareState<UserDetail> createState() => _UserDetailState();
|
||||
}
|
||||
|
||||
class _UserDetailState extends State<UserDetail> with RouteAware {
|
||||
class _UserDetailState extends ScanAwareState<UserDetail> {
|
||||
int _index = 0;
|
||||
String _error = "";
|
||||
bool _loaded = false;
|
||||
User? _user;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
void didPush() {
|
||||
loadData();
|
||||
super.didPush();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
loadData();
|
||||
super.didPopNext();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
routeObserver.unsubscribe(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void loadData() async {
|
||||
final args = ModalRoute.of(context)!.settings.arguments as int;
|
||||
var token = Provider.of<AppState>(context, listen: false).token;
|
||||
|
@ -140,9 +116,11 @@ class _UserDetailState extends State<UserDetail> with RouteAware {
|
|||
}
|
||||
//if (user.avatar != null) {
|
||||
if (user.avatar.startsWith("//")) {
|
||||
user.avatar = "https:" + user.avatar;
|
||||
user.avatar = "https:${user.avatar}";
|
||||
}
|
||||
if (context.mounted) {
|
||||
await precacheImage(NetworkImage(user.avatar), context);
|
||||
}
|
||||
await precacheImage(NetworkImage(user.avatar), context);
|
||||
//}
|
||||
setState(() {
|
||||
_user = user;
|
||||
|
|
670
pubspec.lock
670
pubspec.lock
File diff suppressed because it is too large
Load Diff
23
pubspec.yaml
23
pubspec.yaml
|
@ -15,10 +15,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.3.5+16
|
||||
version: 1.4.1+18
|
||||
|
||||
environment:
|
||||
sdk: ">=2.15.1 <3.0.0"
|
||||
sdk: ">=3.0.0"
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
|
@ -29,25 +29,30 @@ environment:
|
|||
dependencies:
|
||||
html_unescape: ^2.0.0
|
||||
json_annotation: ^4.3.0
|
||||
dropdown_search: ^2.0.1
|
||||
dropdown_search: ^5.0.6
|
||||
provider:
|
||||
pref: ^2.5.0
|
||||
screen_state: ^2.0.0
|
||||
screen_state: ^3.0.1
|
||||
encrypt: ^5.0.1
|
||||
flutter:
|
||||
sdk: flutter
|
||||
catcher_2: ^1.0.7
|
||||
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.2
|
||||
http: ^0.13.4
|
||||
package_info: ^2.0.2
|
||||
http: ^1.2.0
|
||||
package_info_plus: ^5.0.1
|
||||
sentry: ^7.14.0
|
||||
assets_audio_player: ^3.1.1
|
||||
beep_player: ^0.0.1
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_launcher_icons: "^0.9.2"
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
|
||||
build_runner: ^2.0.0
|
||||
json_serializable: ^6.0.0
|
||||
|
@ -57,7 +62,7 @@ dev_dependencies:
|
|||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^1.0.0
|
||||
flutter_lints: ^3.0.1
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
@ -72,6 +77,8 @@ flutter:
|
|||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- assets/audios/
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
|
|
Loading…
Reference in New Issue