Skip to content

Commit

Permalink
The ends of PageViewIndicator are now round (#425)
Browse files Browse the repository at this point in the history
* Add a LinkInputView widget for manual token entry via URL

* Landscape pixel overflow fix

* ends of PageViewIndicator is now round
  • Loading branch information
frankmer authored Nov 8, 2024
1 parent f78f020 commit b1a187c
Show file tree
Hide file tree
Showing 4 changed files with 273 additions and 83 deletions.
174 changes: 92 additions & 82 deletions lib/views/add_token_manually_view/add_token_manually_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import '../../model/tokens/token.dart';
import '../../utils/identifiers.dart';
import '../../utils/logger.dart';
import '../../utils/riverpod_providers.dart';
import 'add_token_manually_view_widgets/PageViewDotIndicator.dart';
import 'add_token_manually_view_widgets/PageViewIndicator.dart';
import 'add_token_manually_view_widgets/labeled_dropdown_button.dart';
import 'add_token_manually_view_widgets/link_input_field.dart';
import 'add_token_manually_view_widgets/link_input_view.dart';

class AddTokenManuallyView extends ConsumerStatefulWidget {
static const routeName = '/add_token_manually';
Expand Down Expand Up @@ -75,7 +75,7 @@ class _AddTokenManuallyViewState extends ConsumerState<AddTokenManuallyView> {
),
body: Column(
children: [
PageViewDotIndicator(
PageViewIndicator(
controller: pageController,
icons: [
Icon(Icons.edit),
Expand All @@ -90,88 +90,98 @@ class _AddTokenManuallyViewState extends ConsumerState<AddTokenManuallyView> {
padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 16.0),
child: Form(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
TextFormField(
key: _labelInputKey,
controller: _labelController,
autovalidateMode: _autoValidateLabel,
focusNode: _labelFieldFocus,
decoration: InputDecoration(labelText: AppLocalizations.of(context)!.name),
validator: (value) {
if (value!.isEmpty) {
return AppLocalizations.of(context)!.pleaseEnterANameForThisToken;
}
return null;
},
),
TextFormField(
key: _secretInputKey,
controller: _secretController,
autovalidateMode: _autoValidateSecret,
focusNode: _secretFieldFocus,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.secretKey,
),
validator: (value) {
if (value!.isEmpty) {
return AppLocalizations.of(context)!.pleaseEnterASecretForThisToken;
} else if ((_typeNotifier.value == TokenTypes.STEAM && Encodings.base32.isInvalidEncoding(value)) ||
(_typeNotifier.value != TokenTypes.STEAM && _encodingNotifier.value.isInvalidEncoding(value))) {
return AppLocalizations.of(context)!.theSecretDoesNotFitTheCurrentEncoding;
}
return null;
},
),
Visibility(
visible: _typeNotifier.value != TokenTypes.STEAM,
child: LabeledDropdownButton<Encodings>(
label: AppLocalizations.of(context)!.encoding,
values: Encodings.values,
valueNotifier: _encodingNotifier,
),
),
Visibility(
visible: _typeNotifier.value != TokenTypes.STEAM,
child: LabeledDropdownButton<Algorithms>(
label: AppLocalizations.of(context)!.algorithm,
values: Algorithms.values.reversed.toList(),
valueNotifier: _algorithmNotifier,
),
),
Visibility(
visible: _typeNotifier.value != TokenTypes.STEAM,
child: LabeledDropdownButton<int>(
label: AppLocalizations.of(context)!.digits,
values: AddTokenManuallyView.allowedDigits,
valueNotifier: _digitsNotifier,
),
),
LabeledDropdownButton<TokenTypes>(
label: AppLocalizations.of(context)!.type,
values: List.from(TokenTypes.values)..remove(TokenTypes.PIPUSH),
valueNotifier: _typeNotifier,
),
Visibility(
// the period is only used by TOTP tokens
visible: _typeNotifier.value == TokenTypes.TOTP,
child: LabeledDropdownButton<int>(
label: AppLocalizations.of(context)!.period,
values: AddTokenManuallyView.allowedPeriodsTOTP,
valueNotifier: _periodNotifier,
postFix: 's' /*seconds*/,
),
),
Visibility(
// the period is only used by DayPassword tokens
visible: _typeNotifier.value == TokenTypes.DAYPASSWORD,
child: LabeledDropdownButton<int>(
label: AppLocalizations.of(context)!.period,
values: AddTokenManuallyView.allowedPeriodsDayPassword,
valueNotifier: _periodDayPasswordNotifier,
postFix: 'h' /*hours*/,
Expanded(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
key: _labelInputKey,
controller: _labelController,
autovalidateMode: _autoValidateLabel,
focusNode: _labelFieldFocus,
decoration: InputDecoration(labelText: AppLocalizations.of(context)!.name),
validator: (value) {
if (value!.isEmpty) {
return AppLocalizations.of(context)!.pleaseEnterANameForThisToken;
}
return null;
},
),
TextFormField(
key: _secretInputKey,
controller: _secretController,
autovalidateMode: _autoValidateSecret,
focusNode: _secretFieldFocus,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.secretKey,
),
validator: (value) {
if (value!.isEmpty) {
return AppLocalizations.of(context)!.pleaseEnterASecretForThisToken;
} else if ((_typeNotifier.value == TokenTypes.STEAM && Encodings.base32.isInvalidEncoding(value)) ||
(_typeNotifier.value != TokenTypes.STEAM && _encodingNotifier.value.isInvalidEncoding(value))) {
return AppLocalizations.of(context)!.theSecretDoesNotFitTheCurrentEncoding;
}
return null;
},
),
Visibility(
visible: _typeNotifier.value != TokenTypes.STEAM,
child: LabeledDropdownButton<Encodings>(
label: AppLocalizations.of(context)!.encoding,
values: Encodings.values,
valueNotifier: _encodingNotifier,
),
),
Visibility(
visible: _typeNotifier.value != TokenTypes.STEAM,
child: LabeledDropdownButton<Algorithms>(
label: AppLocalizations.of(context)!.algorithm,
values: Algorithms.values.reversed.toList(),
valueNotifier: _algorithmNotifier,
),
),
Visibility(
visible: _typeNotifier.value != TokenTypes.STEAM,
child: LabeledDropdownButton<int>(
label: AppLocalizations.of(context)!.digits,
values: AddTokenManuallyView.allowedDigits,
valueNotifier: _digitsNotifier,
),
),
LabeledDropdownButton<TokenTypes>(
label: AppLocalizations.of(context)!.type,
values: List.from(TokenTypes.values)..remove(TokenTypes.PIPUSH),
valueNotifier: _typeNotifier,
),
Visibility(
// the period is only used by TOTP tokens
visible: _typeNotifier.value == TokenTypes.TOTP,
child: LabeledDropdownButton<int>(
label: AppLocalizations.of(context)!.period,
values: AddTokenManuallyView.allowedPeriodsTOTP,
valueNotifier: _periodNotifier,
postFix: 's' /*seconds*/,
),
),
Visibility(
// the period is only used by DayPassword tokens
visible: _typeNotifier.value == TokenTypes.DAYPASSWORD,
child: LabeledDropdownButton<int>(
label: AppLocalizations.of(context)!.period,
values: AddTokenManuallyView.allowedPeriodsDayPassword,
valueNotifier: _periodDayPasswordNotifier,
postFix: 'h' /*hours*/,
),
),
],
),
),
),
Expanded(child: SizedBox.shrink()),
SizedBox(
width: double.infinity,
child: ElevatedButton(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* privacyIDEA Authenticator
*
* Author: Frank Merkel <[email protected]>
*
* Copyright (c) 2024 NetKnights GmbH
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class PageViewIndicator extends ConsumerStatefulWidget {
final PageController controller;
final List<Widget> icons;
const PageViewIndicator({super.key, required this.controller, required this.icons});

@override
ConsumerState<PageViewIndicator> createState() => _PageViewDotIndicatorState();
}

class _PageViewDotIndicatorState extends ConsumerState<PageViewIndicator> {
int _currentPage = 0;

@override
void initState() {
super.initState();
widget.controller.addListener(() {
setState(() {
_currentPage = widget.controller.page?.round() ?? 0;
});
});
}

@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final widthPerIcon = constraints.maxWidth / (widget.icons.length + 1);
final space = widthPerIcon * 0.1;
final double iconWidth = widthPerIcon - space;
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (int i = 0; i < widget.icons.length; i++) ...[
GestureDetector(
onTap: () {
final pageDifference = (i - _currentPage).abs();
widget.controller.animateToPage(i, duration: Duration(milliseconds: 200 * pageDifference + 150), curve: Curves.easeInOut);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: _currentPage == i ? iconWidth * 2 : iconWidth,
decoration: BoxDecoration(
color: _currentPage == i ? Theme.of(context).primaryColor : Theme.of(context).disabledColor,
borderRadius: BorderRadius.circular(99),
),
child: widget.icons[i],
),
),
if (i < widget.icons.length - 1) SizedBox(width: space),
]
],
);
},
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* privacyIDEA Authenticator
*
* Author: Frank Merkel <[email protected]>
*
* Copyright (c) 2024 NetKnights GmbH
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:privacyidea_authenticator/utils/riverpod_providers.dart';

import '../../../l10n/app_localizations.dart';

class LinkInputView extends ConsumerStatefulWidget {
const LinkInputView({super.key});

@override
ConsumerState<LinkInputView> createState() => _LinkInputViewState();
}

class _LinkInputViewState extends ConsumerState<LinkInputView> {
final textController = TextEditingController();

Future<void> addToken(Uri link) async {
if (link.scheme != 'otpauth') {
ref.read(statusMessageProvider.notifier).state = (AppLocalizations.of(context)!.linkMustOtpAuth, '');
return;
}
await ref.read(tokenProvider.notifier).handleLink(link);
if (!mounted) return;
Navigator.of(context).pop();
}

@override
Widget build(BuildContext context) => Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Row(
children: [
Expanded(
child: SingleChildScrollView(
child: TextFormField(
controller: textController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.tokenLink,
),
keyboardType: TextInputType.url,
textInputAction: TextInputAction.done,
onFieldSubmitted: (text) => addToken(Uri.parse(text)),
validator: (value) => value != null
? Uri.tryParse(value) == null
? AppLocalizations.of(context)!.invalidUrl
: null
: null,
),
),
),
SizedBox(width: 8),
IconButton(
icon: Icon(Icons.paste),
onPressed: () async {
ClipboardData? data = await Clipboard.getData('text/plain');
if (data == null || data.text == null || data.text!.isEmpty) {
if (context.mounted) ref.read(statusMessageProvider.notifier).state = (AppLocalizations.of(context)!.clipboardEmpty, '');
return;
}
setState(() => textController.text = data.text ?? '');
},
),
],
),
Expanded(child: SizedBox()),
SizedBox(
width: double.infinity,
child: ElevatedButton(
child: Text(
AppLocalizations.of(context)!.addToken,
style: Theme.of(context).textTheme.headlineSmall,
overflow: TextOverflow.fade,
softWrap: false,
),
onPressed: () => addToken(Uri.parse(textController.text)),
),
),
],
);
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ publish_to: none
# 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: 4.4.2+404201 # TODO Set the right version number
version: 4.4.2+404202 # TODO Set the right version number

# version: major.minor.build + 2x major|2x minor|3x build
# version: version number + build number (optional)
Expand Down

0 comments on commit b1a187c

Please sign in to comment.