feat: tags selection by text field
This commit is contained in:
parent
12635be0d2
commit
5a18fe3746
14 changed files with 312 additions and 62 deletions
|
@ -427,6 +427,10 @@ extension CustomColorScheme on ColorScheme {
|
|||
? const Color.fromRGBO(246, 246, 246, 1)
|
||||
: const Color.fromRGBO(40, 40, 40, 0.6);
|
||||
|
||||
Color get primaryColor => brightness == Brightness.light
|
||||
? const Color(0xFF9610D6)
|
||||
: const Color(0xFF9610D6);
|
||||
|
||||
EnteTheme get enteTheme =>
|
||||
brightness == Brightness.light ? lightTheme : darkTheme;
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"codeIssuerHint": "Issuer",
|
||||
"codeSecretKeyHint": "Secret Key",
|
||||
"codeAccountHint": "Account (you@domain.com)",
|
||||
"codeTagHint": "Tag",
|
||||
"accountKeyType": "Type of key",
|
||||
"sessionExpired": "Session expired",
|
||||
"@sessionExpired": {
|
||||
|
|
|
@ -37,6 +37,7 @@ import 'package:window_manager/window_manager.dart';
|
|||
final _logger = Logger("main");
|
||||
|
||||
Future<void> initSystemTray() async {
|
||||
if (PlatformUtil.isMobile()) return;
|
||||
String path = Platform.isWindows
|
||||
? 'assets/icons/auth-icon.ico'
|
||||
: 'assets/icons/auth-icon.png';
|
||||
|
|
|
@ -22,9 +22,9 @@ class Code {
|
|||
final int counter;
|
||||
bool? hasSynced;
|
||||
|
||||
final CodeDisplay? display;
|
||||
final CodeDisplay display;
|
||||
|
||||
bool get isPinned => display?.pinned ?? false;
|
||||
bool get isPinned => display.pinned;
|
||||
|
||||
Code(
|
||||
this.account,
|
||||
|
@ -37,7 +37,7 @@ class Code {
|
|||
this.counter,
|
||||
this.rawData, {
|
||||
this.generatedID,
|
||||
this.display,
|
||||
required this.display,
|
||||
});
|
||||
|
||||
Code copyWith({
|
||||
|
@ -59,7 +59,7 @@ class Code {
|
|||
final Algorithm updatedAlgo = algorithm ?? this.algorithm;
|
||||
final Type updatedType = type ?? this.type;
|
||||
final int updatedCounter = counter ?? this.counter;
|
||||
final CodeDisplay? updatedDisplay = display ?? this.display;
|
||||
final CodeDisplay updatedDisplay = display ?? this.display;
|
||||
|
||||
return Code(
|
||||
updateAccount,
|
||||
|
@ -80,6 +80,7 @@ class Code {
|
|||
String account,
|
||||
String issuer,
|
||||
String secret,
|
||||
CodeDisplay? display,
|
||||
) {
|
||||
return Code(
|
||||
account,
|
||||
|
@ -91,6 +92,7 @@ class Code {
|
|||
Type.totp,
|
||||
0,
|
||||
"otpauth://totp/$issuer:$account?algorithm=SHA1&digits=6&issuer=$issuer&period=30&secret=$secret",
|
||||
display: display ?? CodeDisplay(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -107,7 +109,7 @@ class Code {
|
|||
_getType(uri),
|
||||
_getCounter(uri),
|
||||
rawData,
|
||||
display: CodeDisplay.fromUri(uri),
|
||||
display: CodeDisplay.fromUri(uri) ?? CodeDisplay(),
|
||||
);
|
||||
} catch (e) {
|
||||
// if account name contains # without encoding,
|
||||
|
@ -154,7 +156,7 @@ class Code {
|
|||
return jsonEncode(
|
||||
Uri.parse(
|
||||
"$rawData&codeDisplay="
|
||||
"${jsonEncode((display ?? CodeDisplay()).toJson())}",
|
||||
"${jsonEncode(display.toJson())}",
|
||||
).toString(),
|
||||
);
|
||||
}
|
||||
|
@ -221,7 +223,7 @@ class Code {
|
|||
}
|
||||
|
||||
static Type _getType(Uri uri) {
|
||||
if (uri.host == "totp" || uri.host == "steam") {
|
||||
if (uri.host == "totp") {
|
||||
return Type.totp;
|
||||
} else if (uri.host == "hotp") {
|
||||
return Type.hotp;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Used to store the display settings of a code.
|
||||
class CodeDisplay {
|
||||
final bool pinned;
|
||||
|
@ -48,6 +50,7 @@ class CodeDisplay {
|
|||
trashed: json['trashed'] ?? false,
|
||||
lastUsedAt: json['lastUsedAt'] ?? 0,
|
||||
tapCount: json['tapCount'] ?? 0,
|
||||
tags: List<String>.from(json['tags'] ?? []),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -65,6 +68,28 @@ class CodeDisplay {
|
|||
'trashed': trashed,
|
||||
'lastUsedAt': lastUsedAt,
|
||||
'tapCount': tapCount,
|
||||
'tags': tags,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is CodeDisplay &&
|
||||
other.pinned == pinned &&
|
||||
other.trashed == trashed &&
|
||||
other.lastUsedAt == lastUsedAt &&
|
||||
other.tapCount == tapCount &&
|
||||
listEquals(other.tags, tags);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return pinned.hashCode ^
|
||||
trashed.hashCode ^
|
||||
lastUsedAt.hashCode ^
|
||||
tapCount.hashCode ^
|
||||
tags.hashCode;
|
||||
}
|
||||
}
|
||||
|
|
26
auth/lib/models/codes.dart
Normal file
26
auth/lib/models/codes.dart
Normal file
|
@ -0,0 +1,26 @@
|
|||
import 'package:ente_auth/models/code.dart';
|
||||
|
||||
class CodeState {
|
||||
final Code? code;
|
||||
final String? error;
|
||||
|
||||
CodeState({
|
||||
required this.code,
|
||||
required this.error,
|
||||
}) : assert(code != null || error != null);
|
||||
}
|
||||
|
||||
class Codes {
|
||||
final List<CodeState> allCodes;
|
||||
final List<String> tags;
|
||||
|
||||
Codes({
|
||||
required this.allCodes,
|
||||
required this.tags,
|
||||
});
|
||||
|
||||
List<Code> get validCodes => allCodes
|
||||
.where((element) => element.code != null)
|
||||
.map((e) => e.code!)
|
||||
.toList();
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/models/code_display.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_result.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
|
@ -8,8 +9,9 @@ import "package:flutter/material.dart";
|
|||
|
||||
class SetupEnterSecretKeyPage extends StatefulWidget {
|
||||
final Code? code;
|
||||
final List<String> tags;
|
||||
|
||||
SetupEnterSecretKeyPage({this.code, super.key});
|
||||
SetupEnterSecretKeyPage({this.code, super.key, required this.tags});
|
||||
|
||||
@override
|
||||
State<SetupEnterSecretKeyPage> createState() =>
|
||||
|
@ -20,7 +22,9 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
|||
late TextEditingController _issuerController;
|
||||
late TextEditingController _accountController;
|
||||
late TextEditingController _secretController;
|
||||
late TextEditingController _tagController;
|
||||
late bool _secretKeyObscured;
|
||||
late List<String> tags = [...?widget.code?.display.tags];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -34,6 +38,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
|||
_secretController = TextEditingController(
|
||||
text: widget.code?.secret,
|
||||
);
|
||||
_tagController = TextEditingController();
|
||||
_secretKeyObscured = widget.code != null;
|
||||
super.initState();
|
||||
}
|
||||
|
@ -50,6 +55,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
// The validator receives the text that the user has entered.
|
||||
|
@ -108,6 +114,54 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
|||
),
|
||||
controller: _accountController,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
TextFormField(
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter some text";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onFieldSubmitted: (str) {
|
||||
if (str.trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tags.contains(str)) {
|
||||
tags.add(str);
|
||||
}
|
||||
|
||||
// Clear the tag
|
||||
_tagController.text = "";
|
||||
setState(() {});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.codeTagHint,
|
||||
),
|
||||
controller: _tagController,
|
||||
),
|
||||
if (tags.isNotEmpty) ...[
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
alignment: WrapAlignment.start,
|
||||
children: tags
|
||||
.map(
|
||||
(e) => TagChip(
|
||||
label: e,
|
||||
onTap: () {
|
||||
tags.remove(e);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
const SizedBox(
|
||||
height: 40,
|
||||
),
|
||||
|
@ -166,16 +220,19 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
|||
return;
|
||||
}
|
||||
}
|
||||
final CodeDisplay display = widget.code!.display.copyWith(tags: tags);
|
||||
final Code newCode = widget.code == null
|
||||
? Code.fromAccountAndSecret(
|
||||
account,
|
||||
issuer,
|
||||
secret,
|
||||
display,
|
||||
)
|
||||
: widget.code!.copyWith(
|
||||
account: account,
|
||||
issuer: issuer,
|
||||
secret: secret,
|
||||
display: display,
|
||||
);
|
||||
// Verify the validity of the code
|
||||
getOTP(newCode);
|
||||
|
@ -196,3 +253,52 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TagChip extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback? onTap;
|
||||
final bool isSelected;
|
||||
|
||||
const TagChip({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.isSelected = false,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Chip(
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.white : const Color(0xFF8232E1),
|
||||
),
|
||||
),
|
||||
if (isSelected) ...[
|
||||
const SizedBox(width: 4),
|
||||
const Icon(
|
||||
Icons.check,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
backgroundColor:
|
||||
isSelected ? const Color(0xFF722ED1) : const Color(0xFFFCF5FF),
|
||||
side: BorderSide(
|
||||
color: isSelected ? const Color(0xFF722ED1) : const Color(0xFFFCF5FF),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:ente_auth/core/event_bus.dart';
|
|||
import 'package:ente_auth/events/codes_updated_event.dart';
|
||||
import 'package:ente_auth/models/authenticator/entity_result.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/models/codes.dart';
|
||||
import 'package:ente_auth/services/authenticator_service.dart';
|
||||
import 'package:ente_auth/store/offline_authenticator_db.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
@ -22,14 +23,16 @@ class CodeStore {
|
|||
_authenticatorService = AuthenticatorService.instance;
|
||||
}
|
||||
|
||||
Future<List<Code>> getAllCodes({AccountMode? accountMode}) async {
|
||||
Future<Codes> getAllCodes({AccountMode? accountMode}) async {
|
||||
final mode = accountMode ?? _authenticatorService.getAccountMode();
|
||||
final List<EntityResult> entities =
|
||||
await _authenticatorService.getEntities(mode);
|
||||
final List<Code> codes = [];
|
||||
final List<CodeState> codes = [];
|
||||
List<String> tags = [];
|
||||
for (final entity in entities) {
|
||||
try {
|
||||
final decodeJson = jsonDecode(entity.rawData);
|
||||
|
||||
late Code code;
|
||||
if (decodeJson is String && decodeJson.startsWith('otpauth://')) {
|
||||
code = Code.fromOTPAuthUrl(decodeJson);
|
||||
|
@ -38,24 +41,38 @@ class CodeStore {
|
|||
}
|
||||
code.generatedID = entity.generatedID;
|
||||
code.hasSynced = entity.hasSynced;
|
||||
codes.add(code);
|
||||
codes.add(CodeState(code: code, error: null));
|
||||
tags.addAll(code.display.tags);
|
||||
} catch (e) {
|
||||
codes.add(CodeState(code: null, error: e.toString()));
|
||||
_logger.severe("Could not parse code", e);
|
||||
}
|
||||
}
|
||||
|
||||
// sort codes by issuer,account
|
||||
codes.sort((a, b) {
|
||||
if (b.isPinned && !a.isPinned) return 1;
|
||||
if (!b.isPinned && a.isPinned) return -1;
|
||||
if (a.code == null && b.code == null) return 0;
|
||||
if (a.code == null) return 1;
|
||||
if (b.code == null) return -1;
|
||||
|
||||
final issuerComparison = compareAsciiLowerCaseNatural(a.issuer, b.issuer);
|
||||
final firstCode = a.code!;
|
||||
final secondCode = b.code!;
|
||||
|
||||
if (secondCode.isPinned && !firstCode.isPinned) return 1;
|
||||
if (!secondCode.isPinned && firstCode.isPinned) return -1;
|
||||
|
||||
final issuerComparison =
|
||||
compareAsciiLowerCaseNatural(firstCode.issuer, secondCode.issuer);
|
||||
if (issuerComparison != 0) {
|
||||
return issuerComparison;
|
||||
}
|
||||
return compareAsciiLowerCaseNatural(a.account, b.account);
|
||||
return compareAsciiLowerCaseNatural(
|
||||
firstCode.account,
|
||||
secondCode.account,
|
||||
);
|
||||
});
|
||||
return codes;
|
||||
tags = tags.toSet().toList();
|
||||
return Codes(allCodes: codes, tags: tags);
|
||||
}
|
||||
|
||||
Future<AddResult> addCode(
|
||||
|
@ -67,7 +84,7 @@ class CodeStore {
|
|||
final codes = await getAllCodes(accountMode: mode);
|
||||
bool isExistingCode = false;
|
||||
bool hasSameCode = false;
|
||||
for (final existingCode in codes) {
|
||||
for (final existingCode in codes.validCodes) {
|
||||
if (code.generatedID != null &&
|
||||
existingCode.generatedID == code.generatedID) {
|
||||
isExistingCode = true;
|
||||
|
@ -124,8 +141,9 @@ class CodeStore {
|
|||
}
|
||||
logger.info('start import');
|
||||
|
||||
List<Code> offlineCodes = await CodeStore.instance
|
||||
.getAllCodes(accountMode: AccountMode.offline);
|
||||
List<Code> offlineCodes = (await CodeStore.instance
|
||||
.getAllCodes(accountMode: AccountMode.offline))
|
||||
.validCodes;
|
||||
if (offlineCodes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
@ -134,8 +152,9 @@ class CodeStore {
|
|||
logger.info("skip as online sync is not done");
|
||||
return;
|
||||
}
|
||||
final List<Code> onlineCodes =
|
||||
await CodeStore.instance.getAllCodes(accountMode: AccountMode.online);
|
||||
final List<Code> onlineCodes = (await CodeStore.instance
|
||||
.getAllCodes(accountMode: AccountMode.online))
|
||||
.validCodes;
|
||||
logger.info(
|
||||
'importing ${offlineCodes.length} offline codes with ${onlineCodes.length} online codes',
|
||||
);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/linear_progress_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
@ -47,9 +48,14 @@ class _CodeTimerProgressState extends State<CodeTimerProgress>
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LinearProgressWidget(
|
||||
color: _progress > 0.4 ? const Color(0xFF9610D6) : Colors.orange,
|
||||
fractionOfStorage: _progress,
|
||||
return SizedBox(
|
||||
height: 3,
|
||||
child: LinearProgressWidget(
|
||||
color: _progress > 0.4
|
||||
? getEnteColorScheme(context).primary700
|
||||
: Colors.orange,
|
||||
fractionOfStorage: _progress,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,8 +27,9 @@ import 'package:move_to_background/move_to_background.dart';
|
|||
|
||||
class CodeWidget extends StatefulWidget {
|
||||
final Code code;
|
||||
final List<String> tags;
|
||||
|
||||
const CodeWidget(this.code, {super.key});
|
||||
const CodeWidget(this.code, this.tags, {super.key});
|
||||
|
||||
@override
|
||||
State<CodeWidget> createState() => _CodeWidgetState();
|
||||
|
@ -500,7 +501,10 @@ class _CodeWidgetState extends State<CodeWidget> {
|
|||
final Code? code = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return SetupEnterSecretKeyPage(code: widget.code);
|
||||
return SetupEnterSecretKeyPage(
|
||||
code: widget.code,
|
||||
tags: widget.tags,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
@ -10,6 +10,7 @@ import 'package:ente_auth/events/icons_changed_event.dart';
|
|||
import 'package:ente_auth/events/trigger_logout_event.dart';
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/models/codes.dart';
|
||||
import 'package:ente_auth/onboarding/view/setup_enter_secret_key_page.dart';
|
||||
import 'package:ente_auth/services/preference_service.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
|
@ -54,8 +55,8 @@ class _HomePageState extends State<HomePage> {
|
|||
final FocusNode searchInputFocusNode = FocusNode();
|
||||
bool _showSearchBox = false;
|
||||
String _searchText = "";
|
||||
List<Code> _codes = [];
|
||||
List<Code> _filteredCodes = [];
|
||||
Codes? _codes;
|
||||
List<CodeState> _filteredCodes = [];
|
||||
StreamSubscription<CodesUpdatedEvent>? _streamSubscription;
|
||||
StreamSubscription<TriggerLogoutEvent>? _triggerLogoutEvent;
|
||||
StreamSubscription<IconsChangedEvent>? _iconsChangedEvent;
|
||||
|
@ -105,26 +106,28 @@ class _HomePageState extends State<HomePage> {
|
|||
}
|
||||
|
||||
void _applyFilteringAndRefresh() {
|
||||
if (_searchText.isNotEmpty && _showSearchBox) {
|
||||
if (_searchText.isNotEmpty && _showSearchBox && _codes != null) {
|
||||
final String val = _searchText.toLowerCase();
|
||||
// Prioritize issuer match above account for better UX while searching
|
||||
// for a specific TOTP for email providers. Searching for "emailProvider" like (gmail, proton) should
|
||||
// show the email provider first instead of other accounts where protonmail
|
||||
// is the account name.
|
||||
final List<Code> issuerMatch = [];
|
||||
final List<Code> accountMatch = [];
|
||||
final List<CodeState> issuerMatch = [];
|
||||
final List<CodeState> accountMatch = [];
|
||||
|
||||
for (final Code code in _codes) {
|
||||
if (code.issuer.toLowerCase().contains(val)) {
|
||||
issuerMatch.add(code);
|
||||
} else if (code.account.toLowerCase().contains(val)) {
|
||||
accountMatch.add(code);
|
||||
for (final CodeState codeState in _codes!.allCodes) {
|
||||
if (codeState.error != null) continue;
|
||||
|
||||
if (codeState.code!.issuer.toLowerCase().contains(val)) {
|
||||
issuerMatch.add(codeState);
|
||||
} else if (codeState.code!.account.toLowerCase().contains(val)) {
|
||||
accountMatch.add(codeState);
|
||||
}
|
||||
}
|
||||
_filteredCodes = issuerMatch;
|
||||
_filteredCodes.addAll(accountMatch);
|
||||
} else {
|
||||
_filteredCodes = _codes;
|
||||
_filteredCodes = _codes?.allCodes ?? [];
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
|
@ -151,7 +154,7 @@ class _HomePageState extends State<HomePage> {
|
|||
if (code != null) {
|
||||
await CodeStore.instance.addCode(code);
|
||||
// Focus the new code by searching
|
||||
if (_codes.length > 2) {
|
||||
if ((_codes?.allCodes.length ?? 0) > 2) {
|
||||
_focusNewCode(code);
|
||||
}
|
||||
}
|
||||
|
@ -161,7 +164,9 @@ class _HomePageState extends State<HomePage> {
|
|||
final Code? code = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return SetupEnterSecretKeyPage();
|
||||
return SetupEnterSecretKeyPage(
|
||||
tags: _codes?.tags ?? [],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -219,6 +224,7 @@ class _HomePageState extends State<HomePage> {
|
|||
focusedBorder: InputBorder.none,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: _showSearchBox
|
||||
|
@ -243,7 +249,7 @@ class _HomePageState extends State<HomePage> {
|
|||
],
|
||||
),
|
||||
floatingActionButton: !_hasLoaded ||
|
||||
_codes.isEmpty ||
|
||||
(_codes?.allCodes.isEmpty ?? true) ||
|
||||
!PreferenceService.instance.hasShownCoachMark()
|
||||
? null
|
||||
: _getFab(),
|
||||
|
@ -260,18 +266,61 @@ class _HomePageState extends State<HomePage> {
|
|||
onManuallySetupTap: _redirectToManualEntryPage,
|
||||
);
|
||||
} else {
|
||||
final list = AlignedGridView.count(
|
||||
crossAxisCount: (MediaQuery.sizeOf(context).width ~/ 400)
|
||||
.clamp(1, double.infinity)
|
||||
.toInt(),
|
||||
itemBuilder: ((context, index) {
|
||||
try {
|
||||
return ClipRect(child: CodeWidget(_filteredCodes[index]));
|
||||
} catch (e) {
|
||||
return const Text("Failed");
|
||||
}
|
||||
}),
|
||||
itemCount: _filteredCodes.length,
|
||||
final list = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 8),
|
||||
itemCount: _codes?.tags == null ? 0 : _codes!.tags.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return const TagChip(
|
||||
label: "All",
|
||||
isSelected: true,
|
||||
);
|
||||
}
|
||||
return TagChip(label: _codes!.tags[index - 1]);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: AlignedGridView.count(
|
||||
crossAxisCount: (MediaQuery.sizeOf(context).width ~/ 400)
|
||||
.clamp(1, double.infinity)
|
||||
.toInt(),
|
||||
itemBuilder: ((context, index) {
|
||||
try {
|
||||
if (_filteredCodes[index].error != null) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
l10n.sorryUnableToGenCode(
|
||||
_filteredCodes[index].code?.issuer ?? "",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ClipRect(
|
||||
child: CodeWidget(
|
||||
_filteredCodes[index].code!,
|
||||
_codes?.tags ?? [],
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
return const Text("Failed");
|
||||
}
|
||||
}),
|
||||
itemCount: _filteredCodes.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
if (!PreferenceService.instance.hasShownCoachMark()) {
|
||||
return Stack(
|
||||
|
@ -291,17 +340,22 @@ class _HomePageState extends State<HomePage> {
|
|||
.clamp(1, double.infinity)
|
||||
.toInt(),
|
||||
itemBuilder: ((context, index) {
|
||||
Code? code;
|
||||
try {
|
||||
code = _filteredCodes[index];
|
||||
return CodeWidget(code);
|
||||
} catch (e, s) {
|
||||
_logger.severe("code widget error", e, s);
|
||||
final codeState = _filteredCodes[index];
|
||||
if (codeState.code != null) {
|
||||
return CodeWidget(
|
||||
codeState.code!,
|
||||
_codes?.tags ?? [],
|
||||
);
|
||||
} else {
|
||||
_logger.severe(
|
||||
"code widget error",
|
||||
codeState.error,
|
||||
);
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
l10n.sorryUnableToGenCode(code?.issuer ?? ""),
|
||||
l10n.sorryUnableToGenCode(""),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -173,7 +173,7 @@ Future<void> _exportCodes(BuildContext context, String fileContent) async {
|
|||
Future<String> _getAuthDataForExport() async {
|
||||
final codes = await CodeStore.instance.getAllCodes();
|
||||
String data = "";
|
||||
for (final code in codes) {
|
||||
for (final code in codes.validCodes) {
|
||||
data += "${code.rawData}\n";
|
||||
}
|
||||
|
||||
|
|
|
@ -95,6 +95,7 @@ Future<int?> _processBitwardenExportFile(
|
|||
account,
|
||||
issuer,
|
||||
totp,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -108,8 +108,9 @@ class SettingsPage extends StatelessWidget {
|
|||
await handleExportClick(context);
|
||||
} else {
|
||||
if (result.action == ButtonAction.second) {
|
||||
bool hasCodes =
|
||||
(await CodeStore.instance.getAllCodes()).isNotEmpty;
|
||||
bool hasCodes = (await CodeStore.instance.getAllCodes())
|
||||
.validCodes
|
||||
.isNotEmpty;
|
||||
if (hasCodes) {
|
||||
final hasAuthenticated = await LocalAuthenticationService
|
||||
.instance
|
||||
|
|
Loading…
Add table
Reference in a new issue