Files
feeddeck/app/lib/widgets/settings/accounts/settings_accounts_github.dart
Rico Berger fb3bec623a [core] Add Missing SafeArea Widget (#124)
Several widgets which where rendered within a modal bottom sheet didn't
used a `SafeArea` widget in the body of the `Scaffold` widget, so that
the action buttons on the bottom of the widget where not rendered in the
correct position.
2024-01-31 20:54:50 +01:00

313 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:provider/provider.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/repositories/profile_repository.dart';
import 'package:feeddeck/utils/api_exception.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/openurl.dart';
import 'package:feeddeck/widgets/general/elevated_button_progress_indicator.dart';
import 'package:feeddeck/widgets/settings/accounts/utils/settings_accounts_actions.dart';
import 'package:feeddeck/widgets/settings/accounts/utils/settings_accounts_item.dart';
const _helpText = '''
Provide a GitHub token to connect your GitHub account to your profile. You can
create a token on in the
[developer settings](https://github.com/settings/tokens) section in the GitHub
account settings.
The token needs the following permissions: **notifications**,
**read:discussion**, **read:org**, **read:project**, **read:user**, **repo**.
You can also use a token with less permissions, but then some features might not
work.
''';
/// The [SettingsAccountsGithub] widget implements the GitHub account section of
/// the settings page. Here the user can add / remove his GitHub account to his
/// profile.
class SettingsAccountsGithub extends StatelessWidget {
const SettingsAccountsGithub({super.key});
Future<void> _deleteAccount(BuildContext context) async {
try {
await Provider.of<ProfileRepository>(
context,
listen: false,
).githubDeleteAccount();
} catch (_) {}
}
@override
Widget build(BuildContext context) {
ProfileRepository profile = Provider.of<ProfileRepository>(
context,
listen: true,
);
return SettingsAccountsItem(
name: FDSourceType.github.toLocalizedString(),
isConnected: profile.accountGithub,
onTap: profile.status == FDProfileStatus.uninitialized
? null
: () {
/// If the user has already connected his GitHub account, we show the
/// accounts so that a user can re-connect his GitHub account or delete
/// the stored token from his profile.
if (profile.accountGithub) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
isDismissible: true,
useSafeArea: true,
elevation: 0,
backgroundColor: Colors.transparent,
constraints: const BoxConstraints(
maxWidth: Constants.centeredFormMaxWidth,
),
builder: (BuildContext context) {
return SettingsAccountsActions(
delete: () {
return _deleteAccount(context);
},
reconnect: () {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
isDismissible: true,
useSafeArea: true,
backgroundColor: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(Constants.spacingMiddle),
),
),
clipBehavior: Clip.antiAliasWithSaveLayer,
constraints: const BoxConstraints(
maxWidth: Constants.centeredFormMaxWidth,
),
builder: (BuildContext context) {
return const SettingsAccountsGithubAdd();
},
);
},
);
},
);
} else {
/// If the user has not yet connected his GitHub account, we show the
/// modal bottom sheet with the form to add a GitHub token to his
/// profile.
showModalBottomSheet(
context: context,
isScrollControlled: true,
isDismissible: true,
useSafeArea: true,
backgroundColor: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(Constants.spacingMiddle),
),
),
clipBehavior: Clip.antiAliasWithSaveLayer,
constraints: const BoxConstraints(
maxWidth: Constants.centeredFormMaxWidth,
),
builder: (BuildContext context) {
return const SettingsAccountsGithubAdd();
},
);
}
},
);
}
}
/// The [SettingsAccountsGithubAdd] widget displays a modal bottom sheet with a
/// form which can be used by the user to provide an token for his GitHub
/// account. When the user clicks on "Add account" button we call the function
/// to add the provided token to his account.
class SettingsAccountsGithubAdd extends StatefulWidget {
const SettingsAccountsGithubAdd({super.key});
@override
State<SettingsAccountsGithubAdd> createState() =>
_SettingsAccountsGithubAddState();
}
class _SettingsAccountsGithubAddState extends State<SettingsAccountsGithubAdd> {
final _formKey = GlobalKey<FormState>();
final _tokenController = TextEditingController();
bool _isLoading = false;
String _error = '';
/// [_validateToken] validates the token provided via the [TextField] of the
/// [_tokenController]. The token field can not be empty.
String? _validateToken(String? value) {
if (value == null || value.isEmpty) {
return 'Token is required';
}
return null;
}
/// [_addAccount] adds the provided GitHub token to the users profile. When we
/// are able to add the token we close the modal bottom sheet. In case of an
/// error we show the error message to the user.
Future<void> _addAccount() async {
if (_formKey.currentState != null && _formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
_error = '';
});
try {
await Provider.of<ProfileRepository>(
context,
listen: false,
).githubAddAccount(_tokenController.text);
setState(() {
_isLoading = false;
_error = '';
});
if (!mounted) return;
Navigator.of(context).pop();
} on ApiException catch (err) {
setState(() {
_isLoading = false;
_error = 'Failed to add account: ${err.message}';
});
} catch (err) {
setState(() {
_isLoading = false;
_error = 'Failed to add account: ${err.toString()}';
});
}
}
}
/// [_buildError] returns a widget to display the [_error] when it is not an
/// empty string.
Widget _buildError() {
if (_error != '') {
return Padding(
padding: const EdgeInsets.all(Constants.spacingMiddle),
child: Text(
_error,
style: const TextStyle(
color: Constants.error,
),
),
);
}
return Container();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
shape: const Border(
bottom: BorderSide(
color: Constants.dividerColor,
width: 1,
),
),
title: Text(
FDSourceType.github.toLocalizedString(),
),
actions: [
IconButton(
icon: const Icon(
Icons.close,
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(Constants.spacingMiddle),
child: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownBody(
selectable: true,
data: _helpText,
onTapLink: (text, href, title) {
try {
if (href != null) {
openUrl(href);
}
} catch (_) {}
},
),
const SizedBox(
height: Constants.spacingMiddle,
),
TextFormField(
controller: _tokenController,
keyboardType: TextInputType.text,
autocorrect: false,
enableSuggestions: true,
maxLines: 1,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Token',
),
validator: (value) => _validateToken(value),
onFieldSubmitted: (value) => _addAccount(),
),
],
),
),
),
),
),
const SizedBox(
height: Constants.spacingSmall,
),
const Divider(
color: Constants.dividerColor,
height: 1,
thickness: 1,
),
_buildError(),
Padding(
padding: const EdgeInsets.all(Constants.spacingMiddle),
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
maximumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
minimumSize: const Size.fromHeight(
Constants.elevatedButtonSize,
),
),
label: const Text('Add Account'),
onPressed: _isLoading ? null : _addAccount,
icon: _isLoading
? const ElevatedButtonProgressIndicator()
: const Icon(Icons.add),
),
),
],
),
),
);
}
}