/* * SPDX-FileCopyrightText: 2019-2021 Vishesh Handa * * SPDX-License-Identifier: AGPL-3.0-or-later */ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:path/path.dart' as p; import 'package:provider/provider.dart'; import 'package:universal_io/io.dart'; import 'package:gitjournal/generated/locale_keys.g.dart'; import 'package:gitjournal/logger/logger.dart'; import 'package:gitjournal/repository.dart'; import 'package:gitjournal/settings/git_config.dart'; import 'package:gitjournal/settings/settings.dart'; import 'package:gitjournal/settings/storage_config.dart'; import 'package:gitjournal/settings/widgets/settings_list_preference.dart'; import 'package:git_setup/screens.dart'; import 'package:git_setup/sshkey.dart'; import 'package:gitjournal/ssh/keygen.dart'; import 'package:gitjournal/utils/utils.dart'; import 'package:gitjournal/widgets/future_builder_with_progress.dart'; class GitRemoteSettingsScreen extends StatefulWidget { static const routePath = '/settings/gitRemote'; @override _GitRemoteSettingsScreenState createState() => _GitRemoteSettingsScreenState(); } class _GitRemoteSettingsScreenState extends State { var branches = []; var remoteHost = ""; var currentBranch = ""; @override Widget build(BuildContext context) { var textTheme = Theme.of(context).textTheme; var settings = Provider.of(context); var gitConfig = Provider.of(context); var repo = Provider.of(context); if (remoteHost.isEmpty) { repo.remoteConfigs().then((list) { setState(() { if (!mounted) return; remoteHost = list.first.url; }); }); } if (branches.isEmpty) { currentBranch = repo.currentBranch ?? ""; repo.branches().then((list) { setState(() { if (!mounted) return; branches = list; }); }); } var body = Column( children: [ if (remoteHost.isNotEmpty) Text( tr(LocaleKeys.settings_gitRemote_host), style: textTheme.bodyText1, textAlign: TextAlign.left, ), if (remoteHost.isNotEmpty) ListTile(title: Text(remoteHost)), if (branches.isNotEmpty) ListPreference( title: tr(LocaleKeys.settings_gitRemote_branch), currentOption: currentBranch, // FIXME options: branches, onChange: (String branch) { var _ = repo.checkoutBranch(branch); setState(() { currentBranch = branch; }); }, ), const SizedBox(height: 8.0), Text( tr(LocaleKeys.setup_sshKeyUserProvided_public), style: textTheme.bodyText1, textAlign: TextAlign.left, ), const SizedBox(height: 16.0), PublicKeyWidget(gitConfig.sshPublicKey), const SizedBox(height: 16.0), const Divider(), Builder( builder: (BuildContext context) => Button( text: tr(LocaleKeys.setup_sshKey_copy), onPressed: () => _copyKeyToClipboard(context), ), ), Builder( builder: (BuildContext context) => Button( text: tr(LocaleKeys.setup_sshKey_regenerate), onPressed: () => _generateSshKey(context), ), ), Builder( builder: (BuildContext context) => Button( text: tr(LocaleKeys.setup_sshKeyChoice_custom), onPressed: _customSshKeys, ), ), ListPreference( title: tr(LocaleKeys.settings_ssh_syncFreq), currentOption: settings.remoteSyncFrequency.toPublicString(), options: RemoteSyncFrequency.options .map((f) => f.toPublicString()) .toList(), onChange: (String publicStr) { var val = RemoteSyncFrequency.fromPublicString(publicStr); settings.remoteSyncFrequency = val; settings.save(); setState(() {}); }, ), RedButton( text: tr(LocaleKeys.settings_gitRemote_changeHost_title), onPressed: _reconfigureGitHost, ), FutureBuilderWithProgress(future: () async { var repo = context.watch(); var result = await repo.canResetHard(); if (result.isFailure) { showResultError(context, result); return const SizedBox(); } var canReset = result.getOrThrow(); if (!canReset) { return const SizedBox(); } return RedButton( text: tr(LocaleKeys.settings_gitRemote_resetHard_title), onPressed: _resetGitHost, ); }()), ], crossAxisAlignment: CrossAxisAlignment.start, ); return Scaffold( appBar: AppBar( title: Text(tr(LocaleKeys.settings_gitRemote_title)), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { Navigator.of(context).pop(); }, ), ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(16.0), child: body, ), ), ); } void _customSshKeys() { var route = MaterialPageRoute( builder: (context) => Scaffold( body: GitHostUserProvidedKeysPage( doneFunction: _updateKeys, saveText: tr(LocaleKeys.setup_sshKey_save), ), appBar: AppBar( title: Text(tr(LocaleKeys.setup_sshKeyChoice_custom)), ), ), settings: const RouteSettings(name: '/settings/gitRemote/customKeys'), ); var _ = Navigator.push(context, route); } void _updateKeys(String publicKey, String privateKey, String password) { var config = Provider.of(context, listen: false); if (publicKey.isEmpty || privateKey.isEmpty) { return; } config.sshPublicKey = publicKey; config.sshPrivateKey = privateKey; config.sshPassword = password; config.save(); Navigator.of(context).pop(); } void _copyKeyToClipboard(BuildContext context) { var gitConfig = context.read(); Clipboard.setData(ClipboardData(text: gitConfig.sshPublicKey)); showSnackbar(context, tr(LocaleKeys.setup_sshKey_copied)); } void _generateSshKey(BuildContext context) { var keyType = context.read().sshKeyType; var comment = "GitJournal-" + Platform.operatingSystem + "-" + DateTime.now().toIso8601String().substring(0, 10); // only the date generateSSHKeys(type: keyType, comment: comment).then((SshKey? sshKey) { var config = Provider.of(context, listen: false); config.sshPublicKey = sshKey!.publicKey; config.sshPrivateKey = sshKey.publicKey; config.sshPassword = sshKey.password; config.save(); Log.d("PublicKey: " + sshKey.publicKey); _copyKeyToClipboard(context); }); } Future _reconfigureGitHost() async { var ok = await showDialog( context: context, builder: (_) => IrreversibleActionConfirmationDialog( title: LocaleKeys.settings_gitRemote_changeHost_title.tr(), subtitle: LocaleKeys.settings_gitRemote_changeHost_subtitle.tr(), ), ); if (ok == null) { return; } var repo = context.read(); var gitDir = repo.gitBaseDirectory; // Figure out the next available folder String repoFolderName = "journal_"; var num = 0; while (true) { var repoFolderPath = p.join(gitDir, "$repoFolderName$num"); if (!Directory(repoFolderPath).existsSync()) { var r = await repo.init(repoFolderPath); showResultError(context, r); break; } num++; } repoFolderName = repoFolderName + num.toString(); var storageConfig = Provider.of(context, listen: false); storageConfig.folderName = repoFolderName; storageConfig.storeInternally = true; await storageConfig.save(); var route = MaterialPageRoute( builder: (context) => GitHostSetupScreen( repoFolderName: repoFolderName, remoteName: 'origin', onCompletedFunction: repo.completeGitHostSetup, ), settings: const RouteSettings(name: '/setupRemoteGit'), ); var _ = await Navigator.push(context, route); Navigator.of(context).popUntil((route) => route.isFirst); } Future _resetGitHost() async { var ok = await showDialog( context: context, builder: (_) => IrreversibleActionConfirmationDialog( title: LocaleKeys.settings_gitRemote_resetHard_title.tr(), subtitle: LocaleKeys.settings_gitRemote_resetHard_subtitle.tr(), ), ); if (ok == null) { return; } var repo = context.read(); var result = await repo.resetHard(); showResultError(context, result); Navigator.of(context).popUntil((route) => route.isFirst); } } class Button extends StatelessWidget { final String text; final void Function() onPressed; const Button({required this.text, required this.onPressed}); @override Widget build(BuildContext context) { return SizedBox( width: double.infinity, child: ElevatedButton( child: Text( text, textAlign: TextAlign.center, style: Theme.of(context).textTheme.button, ), style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Theme.of(context).primaryColor), ), onPressed: onPressed, ), ); } } class RedButton extends StatelessWidget { final String text; final void Function() onPressed; const RedButton({required this.text, required this.onPressed}); @override Widget build(BuildContext context) { return SizedBox( width: double.infinity, child: ElevatedButton( child: Text(text, textAlign: TextAlign.center), style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Colors.red), ), onPressed: onPressed, ), ); } } class IrreversibleActionConfirmationDialog extends StatelessWidget { final String title; final String subtitle; const IrreversibleActionConfirmationDialog( {required this.title, required this.subtitle}); @override Widget build(BuildContext context) { return AlertDialog( title: Text(title), content: Text(subtitle), actions: [ TextButton( child: Text(LocaleKeys.settings_gitRemote_changeHost_cancel.tr()), onPressed: () => Navigator.of(context).pop(), ), TextButton( child: Text(LocaleKeys.settings_gitRemote_changeHost_ok.tr()), onPressed: () => Navigator.of(context).pop(true), ), ], ); } }