ente/lib/ui/code_widget.dart

343 lines
12 KiB
Dart
Raw Normal View History

2022-11-01 06:13:06 +00:00
import 'dart:async';
import 'package:clipboard/clipboard.dart';
2023-09-04 10:43:19 +00:00
import 'package:ente_auth/core/configuration.dart';
2022-11-11 19:04:06 +00:00
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/l10n/l10n.dart';
2022-11-01 06:13:06 +00:00
import 'package:ente_auth/models/code.dart';
2022-11-22 07:19:39 +00:00
import 'package:ente_auth/onboarding/view/setup_enter_secret_key_page.dart';
2023-08-09 08:43:12 +00:00
import 'package:ente_auth/onboarding/view/view_qr_page.dart';
2022-11-01 06:13:06 +00:00
import 'package:ente_auth/store/code_store.dart';
2023-02-11 11:22:08 +00:00
import 'package:ente_auth/ui/code_timer_progress.dart';
2023-08-17 17:24:53 +00:00
import 'package:ente_auth/ui/utils/icon_utils.dart';
import 'package:ente_auth/utils/dialog_util.dart';
2022-11-01 06:13:06 +00:00
import 'package:ente_auth/utils/toast_util.dart';
import 'package:ente_auth/utils/totp_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
2023-02-16 02:17:08 +00:00
import 'package:logging/logging.dart';
2022-11-01 06:13:06 +00:00
class CodeWidget extends StatefulWidget {
final Code code;
const CodeWidget(this.code, {Key? key}) : super(key: key);
@override
State<CodeWidget> createState() => _CodeWidgetState();
}
class _CodeWidgetState extends State<CodeWidget> {
Timer? _everySecondTimer;
2023-02-11 11:22:08 +00:00
final ValueNotifier<String> _currentCode = ValueNotifier<String>("");
final ValueNotifier<String> _nextCode = ValueNotifier<String>("");
2023-02-16 02:17:08 +00:00
final Logger logger = Logger("_CodeWidgetState");
2023-08-01 07:11:04 +00:00
bool _isInitialized = false;
2023-09-04 10:43:19 +00:00
late bool hasConfiguredAccount;
2022-11-01 06:13:06 +00:00
@override
void initState() {
super.initState();
2023-08-01 07:11:04 +00:00
_everySecondTimer =
2023-02-11 11:22:08 +00:00
Timer.periodic(const Duration(milliseconds: 500), (Timer t) {
String newCode = _getCurrentOTP();
2023-02-11 11:22:08 +00:00
if (newCode != _currentCode.value) {
_currentCode.value = newCode;
if (widget.code.type == Type.totp) {
_nextCode.value = _getNextTotp();
}
2023-02-11 11:22:08 +00:00
}
2022-11-01 06:13:06 +00:00
});
2023-09-04 10:43:19 +00:00
hasConfiguredAccount = Configuration.instance.hasConfiguredAccount();
2022-11-01 06:13:06 +00:00
}
@override
void dispose() {
_everySecondTimer?.cancel();
2023-02-11 11:22:08 +00:00
_currentCode.dispose();
_nextCode.dispose();
2022-11-01 06:13:06 +00:00
super.dispose();
}
@override
Widget build(BuildContext context) {
2023-08-01 07:11:04 +00:00
if (!_isInitialized) {
_currentCode.value = _getCurrentOTP();
if (widget.code.type == Type.totp) {
_nextCode.value = _getNextTotp();
}
2023-08-01 07:11:04 +00:00
_isInitialized = true;
}
final l10n = context.l10n;
2022-11-11 19:04:06 +00:00
return Container(
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8),
child: Slidable(
key: ValueKey(widget.code.hashCode),
endActionPane: ActionPane(
2023-08-09 08:43:12 +00:00
extentRatio: 0.60,
2022-11-11 19:04:06 +00:00
motion: const ScrollMotion(),
children: [
2023-08-09 08:43:12 +00:00
const SizedBox(
width: 4,
),
SlidableAction(
onPressed: _onShowQrPressed,
backgroundColor: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
foregroundColor:
2023-08-17 17:24:53 +00:00
Theme.of(context).colorScheme.inverseBackgroundColor,
2023-08-09 08:43:12 +00:00
icon: Icons.qr_code_2_outlined,
label: "QR",
padding: const EdgeInsets.only(left: 4, right: 0),
spacing: 8,
),
const SizedBox(
width: 4,
),
2022-11-22 07:19:39 +00:00
SlidableAction(
onPressed: _onEditPressed,
backgroundColor: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
foregroundColor:
Theme.of(context).colorScheme.inverseBackgroundColor,
icon: Icons.edit_outlined,
2023-04-08 04:08:10 +00:00
label: l10n.edit,
2022-11-22 07:19:39 +00:00
padding: const EdgeInsets.only(left: 4, right: 0),
spacing: 8,
),
const SizedBox(
width: 4,
),
2022-11-11 19:04:06 +00:00
SlidableAction(
onPressed: _onDeletePressed,
backgroundColor: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
foregroundColor: const Color(0xFFFE4A49),
icon: Icons.delete,
2023-04-08 04:08:10 +00:00
label: l10n.delete,
2022-11-11 19:04:06 +00:00
padding: const EdgeInsets.only(left: 0, right: 0),
2022-11-22 07:19:39 +00:00
spacing: 8,
2022-11-11 19:04:06 +00:00
),
],
),
2023-07-31 10:26:05 +00:00
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
child: Material(
color: Colors.transparent,
child: InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
onTap: () {
_copyToClipboard();
},
onLongPress: () {
_copyToClipboard();
},
child: SizedBox(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.code.type == Type.totp)
CodeTimerProgress(
period: widget.code.period,
),
2023-07-31 10:26:05 +00:00
const SizedBox(
height: 16,
),
Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
safeDecode(widget.code.issuer).trim(),
2023-08-22 04:47:15 +00:00
style: Theme.of(context).textTheme.titleLarge,
2023-07-31 10:26:05 +00:00
),
const SizedBox(height: 2),
Text(
safeDecode(widget.code.account).trim(),
style: Theme.of(context)
.textTheme
2023-08-22 04:47:15 +00:00
.bodySmall
2023-07-31 10:26:05 +00:00
?.copyWith(
fontSize: 12,
color: Colors.grey,
),
),
],
),
2023-08-17 17:24:53 +00:00
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
2023-09-04 10:43:19 +00:00
(widget.code.hasSynced != null &&
widget.code.hasSynced!) || !hasConfiguredAccount
2023-09-04 10:52:50 +00:00
? const SizedBox.shrink()
2023-08-17 17:24:53 +00:00
: const Icon(
Icons.sync_disabled,
size: 20,
color: Colors.amber,
),
const SizedBox(width: 12),
IconUtils.instance.getIcon(
safeDecode(widget.code.issuer).trim(),
),
],
),
2023-07-31 10:26:05 +00:00
],
2022-11-11 19:07:06 +00:00
),
2023-07-31 10:26:05 +00:00
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: ValueListenableBuilder<String>(
valueListenable: _currentCode,
builder: (context, value, child) {
return Text(
value,
style: const TextStyle(fontSize: 24),
);
},
),
),
widget.code.type == Type.totp
? Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
l10n.nextTotpTitle,
style:
2023-08-22 04:47:15 +00:00
Theme.of(context).textTheme.bodySmall,
2023-07-31 10:26:05 +00:00
),
ValueListenableBuilder<String>(
valueListenable: _nextCode,
builder: (context, value, child) {
return Text(
value,
style: const TextStyle(
fontSize: 18,
color: Colors.grey,
),
);
},
),
],
)
: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
l10n.nextTotpTitle,
style:
2023-08-22 04:47:15 +00:00
Theme.of(context).textTheme.bodySmall,
),
2023-08-17 17:24:53 +00:00
InkWell(
onTap: _onNextHotpTapped,
child: const Icon(
Icons.forward_outlined,
size: 32,
color: Colors.grey,
),
),
],
),
2023-07-31 10:26:05 +00:00
],
2022-11-11 19:04:06 +00:00
),
2023-07-31 10:26:05 +00:00
),
const SizedBox(
height: 20,
),
],
2022-11-11 19:04:06 +00:00
),
2022-11-01 06:13:06 +00:00
),
),
2022-11-11 19:04:06 +00:00
),
2022-11-01 06:13:06 +00:00
),
),
),
);
}
void _copyToClipboard() {
FlutterClipboard.copy(_getCurrentOTP())
2023-04-08 04:08:10 +00:00
.then((value) => showToast(context, context.l10n.copiedToClipboard));
}
void _onNextHotpTapped() {
2023-08-17 17:24:53 +00:00
if (widget.code.type == Type.hotp) {
CodeStore.instance
.addCode(
widget.code.copyWith(counter: widget.code.counter + 1),
shouldSync: true,
)
.ignore();
}
}
2022-11-22 07:19:39 +00:00
Future<void> _onEditPressed(_) async {
final Code? code = await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return SetupEnterSecretKeyPage(code: widget.code);
},
),
);
if (code != null) {
CodeStore.instance.addCode(code);
}
}
2023-08-09 08:43:12 +00:00
Future<void> _onShowQrPressed(_) async {
final Code? code = await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return ViewQrPage(code: widget.code);
},
),
);
}
void _onDeletePressed(_) async {
final l10n = context.l10n;
await showChoiceActionSheet(
context,
title: l10n.deleteCodeTitle,
body: l10n.deleteCodeMessage,
firstButtonLabel: l10n.delete,
isCritical: true,
firstButtonOnTap: () async {
await CodeStore.instance.removeCode(widget.code);
2022-11-01 06:13:06 +00:00
},
);
}
String _getCurrentOTP() {
2022-11-01 06:13:06 +00:00
try {
return getOTP(widget.code);
2022-11-01 06:13:06 +00:00
} catch (e) {
2023-04-08 04:08:10 +00:00
return context.l10n.error;
2022-11-01 06:13:06 +00:00
}
}
String _getNextTotp() {
try {
assert(widget.code.type == Type.totp);
2022-11-01 06:13:06 +00:00
return getNextTotp(widget.code);
} catch (e) {
2023-04-08 04:08:10 +00:00
return context.l10n.error;
2022-11-01 06:13:06 +00:00
}
}
}