Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 物品管理位置支持生成和扫描二维码 #394

Merged
merged 6 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"editor.codeActionsOnSave": {
"source.organizeImports": true
"source.organizeImports": "explicit"
},
"dart.showTodos": false,
}
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/lang/zh-CN/

## [Unreleased]

### Added

- 物品管理位置支持生成和扫描二维码

### Fixed

- 修复网页浏览无法返回的问题
Expand Down
5 changes: 5 additions & 0 deletions lib/storage/model/popup_menu.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ enum ItemDetailMenu { edit, delete, consumable, addPicture }
///
/// 删除
enum PictureMenu { delete }

/// 位置详情
///
/// 编辑,删除,二维码
enum StorageDetailMenu { edit, delete, qr }
2 changes: 2 additions & 0 deletions lib/storage/view/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:smarthome/core/core.dart';
import 'package:smarthome/routers/delegate.dart';
import 'package:smarthome/storage/storage.dart';
import 'package:smarthome/storage/view/item_edit_page.dart';
import 'package:smarthome/storage/view/widgets/scan_qr_icon_button.dart';
import 'package:smarthome/storage/view/widgets/search_icon_button.dart';
import 'package:smarthome/utils/constants.dart';
import 'package:smarthome/utils/date_format_extension.dart';
Expand Down Expand Up @@ -46,6 +47,7 @@ class StorageHomeScreen extends StatelessWidget {
return MyHomePage(
activeTab: AppTab.storage,
actions: const <Widget>[
ScanQRIconButton(),
SearchIconButton(),
],
floatingActionButton: FloatingActionButton(
Expand Down
30 changes: 23 additions & 7 deletions lib/storage/view/storage_datail_page.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:smarthome/core/core.dart';
import 'package:smarthome/routers/delegate.dart';
import 'package:smarthome/storage/model/popup_menu.dart';
import 'package:smarthome/storage/storage.dart';
import 'package:smarthome/storage/view/item_edit_page.dart';
import 'package:smarthome/storage/view/storage_edit_page.dart';
import 'package:smarthome/storage/view/storage_qr_page.dart';
import 'package:smarthome/storage/view/widgets/add_storage_icon_button.dart';
import 'package:smarthome/storage/view/widgets/search_icon_button.dart';
import 'package:smarthome/storage/view/widgets/storage_item_list.dart';
Expand Down Expand Up @@ -94,12 +95,14 @@ class StorageDetailScreen extends StatelessWidget {
),
const SearchIconButton(),
if (state.storage.id != homeStorage.id)
PopupMenuButton<Menu>(
PopupMenuButton<StorageDetailMenu>(
onSelected: (value) async {
if (value == Menu.edit) {
final navigator = Navigator.of(context);

if (value == StorageDetailMenu.edit) {
final storageDetailBloc = context.read<StorageDetailBloc>();

final r = await Navigator.of(context).push(
final r = await navigator.push(
MaterialPageRoute(
builder: (_) => BlocProvider<StorageEditBloc>(
create: (_) => StorageEditBloc(
Expand All @@ -119,7 +122,7 @@ class StorageDetailScreen extends StatelessWidget {
);
}
}
if (value == Menu.delete) {
if (value == StorageDetailMenu.delete) {
if (context.mounted) {
await showDialog(
context: context,
Expand Down Expand Up @@ -147,16 +150,29 @@ class StorageDetailScreen extends StatelessWidget {
);
}
}
if (value == StorageDetailMenu.qr) {
await navigator.push(
MaterialPageRoute(
builder: (_) => StorageQRPage(
storage: state.storage,
),
),
);
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: Menu.edit,
value: StorageDetailMenu.edit,
child: Text('编辑'),
),
const PopupMenuItem(
value: Menu.delete,
value: StorageDetailMenu.delete,
child: Text('删除'),
),
const PopupMenuItem(
value: StorageDetailMenu.qr,
child: Text('二维码'),
)
],
)
],
Expand Down
37 changes: 37 additions & 0 deletions lib/storage/view/storage_qr_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:smarthome/storage/model/models.dart';
import 'package:smarthome/storage/view/widgets/share_icon_button.dart';

class StorageQRPage extends StatelessWidget {
final Storage storage;

const StorageQRPage({super.key, required this.storage});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('二维码'),
actions: [
ShareQrIconButton(name: storage.name, data: storage.id),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
QrImageView(
data: storage.id,
size: 300.0,
version: QrVersions.auto,
gapless: true,
),
Text('ID: ${storage.id}'),
Text('名称: ${storage.name}'),
],
),
),
);
}
}
158 changes: 158 additions & 0 deletions lib/storage/view/widgets/scan_qr_icon_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
import 'package:smarthome/core/model/grobal_keys.dart';
import 'package:smarthome/routers/delegate.dart';
import 'package:smarthome/storage/storage.dart';

class ScanQRIconButton extends StatelessWidget {
const ScanQRIconButton({super.key});

@override
Widget build(BuildContext context) {
return Tooltip(
message: '扫描二维码',
child: IconButton(
icon: const Icon(Icons.qr_code),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const ScanQRPage(),
),
);
},
),
);
}
}

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

@override
State<ScanQRPage> createState() => _ScanQRPageState();
}

class _ScanQRPageState extends State<ScanQRPage> {
bool jumped = false;
QRViewController? controller;
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');

// In order to get hot reload to work we need to pause the camera if the platform
// is android, or resume the camera if the platform is iOS.
@override
void reassemble() {
super.reassemble();
if (Platform.isAndroid) {
controller!.pauseCamera();
}
controller!.resumeCamera();
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Expanded(
flex: 4,
child: GestureDetector(
child: _buildQrView(context),
onTap: () async {
jumped = false;
await controller?.resumeCamera();
},
)),
Expanded(
flex: 1,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('请扫描二维码'),
Container(
margin: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: () async {
await controller?.toggleFlash();
setState(() {});
},
child: FutureBuilder(
future: controller?.getFlashStatus(),
builder: (context, snapshot) {
return Text(
snapshot.data ?? false ? '关闭闪光灯' : '打开闪光灯');
},
)),
),
],
),
)
],
),
);
}

Widget _buildQrView(BuildContext context) {
// For this example we check how width or tall the device is and change the scanArea and overlay accordingly.
var scanArea = (MediaQuery.of(context).size.width < 400 ||
MediaQuery.of(context).size.height < 400)
? 150.0
: 300.0;
// To ensure the Scanner view is properly sizes after rotation
// we need to listen for Flutter SizeChanged notification and update controller
return QRView(
key: qrKey,
onQRViewCreated: _onQRViewCreated,
overlay: QrScannerOverlayShape(
borderColor: Colors.red,
borderRadius: 10,
borderLength: 30,
borderWidth: 10,
cutOutSize: scanArea),
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
);
}

void _onQRViewCreated(QRViewController controller) {
setState(() {
this.controller = controller;
});
controller.scannedDataStream.listen((scanData) {
if (jumped) {
return;
}
final storageId = scanData.code;
if (storageId == null) {
return;
}
if (validateStorageId(storageId)) {
MyRouterDelegate.of(context)
.push(StorageDetailPage(storageId: storageId));
jumped = true;
controller.pauseCamera();
}
});
}

void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) {
if (!p) {
scaffoldMessengerKey.currentState!.showSnackBar(
const SnackBar(content: Text('没有相机权限')),
);
}
}

@override
void dispose() {
controller?.dispose();
super.dispose();
}

bool validateStorageId(String id) {
Codec<String, String> stringToBase64 = utf8.fuse(base64);
final decoded = stringToBase64.decode(id);
return decoded.startsWith('Storage:');
}
}
59 changes: 59 additions & 0 deletions lib/storage/view/widgets/share_icon_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
import 'package:smarthome/core/model/grobal_keys.dart';

class ShareQrIconButton extends StatelessWidget {
final String name;
final String data;

const ShareQrIconButton({super.key, required this.data, required this.name});

@override
Widget build(BuildContext context) {
return Tooltip(
message: '分享',
child: IconButton(
icon: const Icon(Icons.share),
onPressed: () async {
final painter = QrPainter(
data: data,
version: QrVersions.auto,
gapless: true,
);
final imageData = await painter.toImageData(600);
final imageBytes = imageData?.buffer.asUint8List();
// share image
if (imageBytes == null) {
return;
}
final shareResult = await Share.shareXFiles(
[
// NOTE: https://github.com/fluttercommunity/plus_plugins/issues/1548
// share_plus 不会使用 XFile 的名称
XFile.fromData(
imageBytes,
mimeType: 'image/png',
name: '$name.png',
)
],
text: '二维码',
);
if (shareResult.status == ShareResultStatus.success) {
scaffoldMessengerKey.currentState!.showSnackBar(
const SnackBar(
content: Text('分享成功'),
),
);
} else {
scaffoldMessengerKey.currentState!.showSnackBar(
const SnackBar(
content: Text('分享失败'),
),
);
}
},
),
);
}
}
Loading
Loading