huge update, such refactoring much doku (noch nich alles), 6.2.3 compatibility, dependency updates

This commit is contained in:
henne 2024-01-26 01:05:36 +01:00
parent 2c9d1b7d75
commit 5418e49ff3
65 changed files with 1244 additions and 1037 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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) {

View File

@ -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)
}
}

View File

@ -26,6 +26,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

BIN
assets/audios/allright.wav Normal file

Binary file not shown.

BIN
assets/audios/fail.mp3 Normal file

Binary file not shown.

BIN
assets/audios/fail.wav Normal file

Binary file not shown.

BIN
assets/audios/success.mp3 Normal file

Binary file not shown.

BIN
assets/audios/success.wav Normal file

Binary file not shown.

View File

@ -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 ]

0
initialize.sh Normal file → Executable file
View File

View File

@ -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))
],
))
],

View File

@ -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),
),
),
)
])
],
),
);
}
}

View File

@ -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,

View File

@ -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),
);
}

View File

@ -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));
}
}

View File

@ -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),
),

View File

@ -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> {

View File

@ -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(

View File

@ -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> {

View File

@ -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!),
)
],
))

View File

@ -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,
)),
],
),
],

View File

@ -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,
),

View File

@ -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),
),
)
),
)
])
],
),

View File

@ -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(),
},
);
}

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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;
}

View File

@ -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(),

View File

@ -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);
}

View File

@ -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,
};

View File

@ -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);

View File

@ -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 {

View File

@ -0,0 +1,5 @@
abstract class ComparableWithId {
int id = 0;
String getName();
bool equals(ComparableWithId other);
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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;
}

View File

@ -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 =

View File

@ -1,3 +1,6 @@
/*
Here are all the http requests to handle locations
*/
import 'dart:convert';
import 'dart:io';

View File

@ -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'
});

View File

@ -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();
}

View File

@ -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;
}

36
lib/utils/audio.dart Normal file
View File

@ -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);
}

26
lib/utils/logging.dart Normal file
View File

@ -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.
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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';

View File

@ -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)

View File

@ -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"]);

View File

@ -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(

View File

@ -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"]);

View File

@ -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(),

View File

@ -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(),

View File

@ -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(

View File

@ -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")
],

View File

@ -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')
])
],
));
}

View File

@ -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(() {

View File

@ -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;

File diff suppressed because it is too large Load Diff

View File

@ -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: