From 18db4899b2c0d41c8726238c6f4c4af3788de36e Mon Sep 17 00:00:00 2001 From: Vishesh Handa Date: Fri, 15 May 2020 15:28:44 +0200 Subject: [PATCH] GitHostSetup: Allow the user to choose between repos This way they don't need to use a repo called 'journal'. --- lib/apis/githost.dart | 15 +- lib/apis/github.dart | 7 + lib/apis/gitlab.dart | 8 + lib/setup/autoconfigure.dart | 34 +--- lib/setup/autoconfigure_complete.dart | 90 ++++++++++ lib/setup/repo_selector.dart | 228 ++++++++++++++++++++++++++ lib/setup/screens.dart | 42 ++++- 7 files changed, 381 insertions(+), 43 deletions(-) create mode 100644 lib/setup/autoconfigure_complete.dart create mode 100644 lib/setup/repo_selector.dart diff --git a/lib/apis/githost.dart b/lib/apis/githost.dart index 03db6074..1704f788 100644 --- a/lib/apis/githost.dart +++ b/lib/apis/githost.dart @@ -12,7 +12,7 @@ abstract class GitHost { Future> listRepos(); Future createRepo(String name); Future getRepo(String name); - Future addDeployKey(String sshPublicKey, String repo); + Future addDeployKey(String sshPublicKey, String repoFullName); } class UserInfo { @@ -28,14 +28,19 @@ class UserInfo { } class GitHostRepo { - String fullName; - String cloneUrl; + final String fullName; + final String cloneUrl; + final DateTime updatedAt; - GitHostRepo({this.fullName, this.cloneUrl}); + GitHostRepo({ + @required this.fullName, + @required this.cloneUrl, + @required this.updatedAt, + }); @override String toString() { - return 'GitRepo{fulleName: $fullName, cloneUrl: $cloneUrl}'; + return 'GitRepo{fulleName: $fullName, cloneUrl: $cloneUrl, updatedAt: $updatedAt}'; } } diff --git a/lib/apis/github.dart b/lib/apis/github.dart index 587ef306..b9af0195 100644 --- a/lib/apis/github.dart +++ b/lib/apis/github.dart @@ -210,9 +210,16 @@ class GitHub implements GitHost { } GitHostRepo _repoFromJson(Map parsedJson) { + DateTime updatedAt; + try { + updatedAt = DateTime.parse(parsedJson['updated_at'].toString()); + } catch (e) { + Log.e(e); + } return GitHostRepo( fullName: parsedJson['full_name'], cloneUrl: parsedJson['ssh_url'], + updatedAt: updatedAt, ); } diff --git a/lib/apis/gitlab.dart b/lib/apis/gitlab.dart index 1e0d1cbb..66fcacaf 100644 --- a/lib/apis/gitlab.dart +++ b/lib/apis/gitlab.dart @@ -194,9 +194,17 @@ class GitLab implements GitHost { } GitHostRepo _repoFromJson(Map parsedJson) { + DateTime updatedAt; + try { + updatedAt = DateTime.parse(parsedJson['last_activity_at'].toString()); + } catch (e) { + Log.e(e); + } + return GitHostRepo( fullName: parsedJson['path_with_namespace'], cloneUrl: parsedJson['ssh_url_to_repo'], + updatedAt: updatedAt, ); } diff --git a/lib/setup/autoconfigure.dart b/lib/setup/autoconfigure.dart index daf1505c..b0330833 100644 --- a/lib/setup/autoconfigure.dart +++ b/lib/setup/autoconfigure.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:function_types/function_types.dart'; -import 'package:git_bindings/git_bindings.dart'; - import 'package:gitjournal/analytics.dart'; import 'package:gitjournal/apis/githost_factory.dart'; import 'package:gitjournal/error_reporting.dart'; @@ -16,7 +14,7 @@ import 'loading.dart'; class GitHostSetupAutoConfigure extends StatefulWidget { final GitHostType gitHostType; - final Func1 onDone; + final Func1 onDone; GitHostSetupAutoConfigure({ @required this.gitHostType, @@ -50,35 +48,11 @@ class GitHostSetupAutoConfigureState extends State { } Log.d("GitHost Initalized: " + widget.gitHostType.toString()); - GitHostRepo repo; try { setState(() { - _message = "Creating private repo"; + _message = "Reading User Info"; }); - try { - repo = await gitHost.createRepo("journal"); - } on GitHostException catch (e) { - if (e.cause != GitHostException.RepoExists.cause) { - rethrow; - } - - setState(() { - _message = "Using existing repo"; - }); - repo = await gitHost.getRepo("journal"); - } - - setState(() { - _message = "Generating SSH Key"; - }); - var publicKey = await generateSSHKeys(comment: "GitJournal"); - - setState(() { - _message = "Adding as a Deploy Key"; - }); - await gitHost.addDeployKey(publicKey, repo.fullName); - var userInfo = await gitHost.getUserInfo(); if (userInfo.name != null && userInfo.name.isNotEmpty) { Settings.instance.gitAuthor = userInfo.name; @@ -91,7 +65,7 @@ class GitHostSetupAutoConfigureState extends State { _handleGitHostException(e, stacktrace); return; } - widget.onDone(repo.cloneUrl); + widget.onDone(gitHost); }); try { @@ -143,7 +117,7 @@ class GitHostSetupAutoConfigureState extends State { // Step 1 Text( - "1. Create a new private repo called 'journal' or use the existing one", + "1. List your existing repos or create a new repo", style: Theme.of(context).textTheme.bodyText1, ), const SizedBox(height: 8.0), diff --git a/lib/setup/autoconfigure_complete.dart b/lib/setup/autoconfigure_complete.dart new file mode 100644 index 00000000..a7cb581b --- /dev/null +++ b/lib/setup/autoconfigure_complete.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:function_types/function_types.dart'; + +import 'package:git_bindings/git_bindings.dart'; + +import 'package:gitjournal/analytics.dart'; +import 'package:gitjournal/apis/githost_factory.dart'; +import 'package:gitjournal/error_reporting.dart'; +import 'package:gitjournal/utils/logger.dart'; + +import 'error.dart'; +import 'loading.dart'; + +class GitHostSetupAutoConfigureComplete extends StatefulWidget { + final GitHost gitHost; + final GitHostRepo repo; + final Func1 onDone; + + GitHostSetupAutoConfigureComplete({ + @required this.gitHost, + @required this.repo, + @required this.onDone, + }); + + @override + GitHostSetupAutoConfigureCompleteState createState() { + return GitHostSetupAutoConfigureCompleteState(); + } +} + +class GitHostSetupAutoConfigureCompleteState + extends State { + String errorMessage = ""; + + String _message = "..."; + + @override + void initState() { + super.initState(); + _initAsync(); + } + + void _initAsync() async { + Log.d("Starting autoconfigure copletion"); + + try { + Log.i("Generating SSH Key"); + setState(() { + _message = "Generating SSH Key"; + }); + var publicKey = await generateSSHKeys(comment: "GitJournal"); + + Log.i("Adding as a deploy key"); + if (mounted) { + setState(() { + _message = "Adding as a Deploy Key"; + }); + } + await widget.gitHost.addDeployKey(publicKey, widget.repo.fullName); + } on Exception catch (e, stacktrace) { + _handleGitHostException(e, stacktrace); + return; + } + widget.onDone(widget.repo.cloneUrl); + } + + void _handleGitHostException(Exception e, StackTrace stacktrace) { + Log.d("GitHostSetupAutoConfigureComplete: " + e.toString()); + setState(() { + errorMessage = e.toString(); + getAnalytics().logEvent( + name: "githostsetup_error", + parameters: { + 'errorMessage': errorMessage, + }, + ); + + logException(e, stacktrace); + }); + } + + @override + Widget build(BuildContext context) { + if (errorMessage == null || errorMessage.isEmpty) { + return GitHostSetupLoadingPage(_message); + } + + return GitHostSetupErrorPage(errorMessage); + } +} diff --git a/lib/setup/repo_selector.dart b/lib/setup/repo_selector.dart new file mode 100644 index 00000000..6ae98463 --- /dev/null +++ b/lib/setup/repo_selector.dart @@ -0,0 +1,228 @@ +import 'package:flutter/material.dart'; +import 'package:function_types/function_types.dart'; + +import 'package:gitjournal/analytics.dart'; +import 'package:gitjournal/apis/githost_factory.dart'; +import 'package:gitjournal/error_reporting.dart'; +import 'package:gitjournal/utils/logger.dart'; +import 'package:intl/intl.dart'; + +import 'button.dart'; +import 'error.dart'; +import 'loading.dart'; + +class GitHostSetupRepoSelector extends StatefulWidget { + final GitHost gitHost; + final Func1 onDone; + + GitHostSetupRepoSelector({ + @required this.gitHost, + @required this.onDone, + }); + + @override + GitHostSetupRepoSelectorState createState() { + return GitHostSetupRepoSelectorState(); + } +} + +class GitHostSetupRepoSelectorState extends State { + String errorMessage = ""; + + List repos = []; + var fetchedRepos = false; + + GitHostRepo selectedRepo; + var _textController = TextEditingController(); + bool createRepo = false; + + @override + void initState() { + super.initState(); + + _textController.addListener(() { + setState(() { + selectedRepo = null; + createRepo = false; + }); + }); + _initStateAysnc(); + } + + void _initStateAysnc() async { + Log.d("Starting RepoSelector"); + + try { + var allRepos = await widget.gitHost.listRepos(); + allRepos.sort((GitHostRepo a, GitHostRepo b) { + if (a.updatedAt != null && b.updatedAt != null) { + return a.updatedAt.compareTo(b.updatedAt); + } + if (a.updatedAt == null && b.updatedAt == null) { + return a.fullName.compareTo(b.fullName); + } + if (a.updatedAt == null) { + return 1; + } + return -1; + }); + + if (!mounted) return; + setState(() { + repos = allRepos.reversed.toList(); + fetchedRepos = true; + }); + + var repo = repos.firstWhere( + (r) => r.fullName.endsWith('/journal'), + orElse: () => null, + ); + if (repo != null) { + setState(() { + selectedRepo = repo; + }); + } else { + setState(() { + _textController.text = "journal"; + createRepo = true; + }); + } + } on Exception catch (e, stacktrace) { + _handleGitHostException(e, stacktrace); + return; + } + } + + void _handleGitHostException(Exception e, StackTrace stacktrace) { + Log.d("GitHostSetupAutoConfigure: " + e.toString()); + setState(() { + errorMessage = e.toString(); + getAnalytics().logEvent( + name: "githostsetup_error", + parameters: { + 'errorMessage': errorMessage, + }, + ); + + logException(e, stacktrace); + }); + } + + @override + Widget build(BuildContext context) { + if (errorMessage != null && errorMessage.isNotEmpty) { + return GitHostSetupErrorPage(errorMessage); + } + if (!fetchedRepos) { + return GitHostSetupLoadingPage("Loading"); + } + + var q = _textController.text.toLowerCase(); + var filteredRepos = repos.where((r) { + var repoName = r.fullName.split('/').last; + return repoName.toLowerCase().contains(q); + }); + + var repoBuilder = ListView( + children: [ + if (_textController.text.isNotEmpty) _buildCreateRepoTile(), + for (var repo in filteredRepos) _buildRepoTile(repo), + ], + padding: const EdgeInsets.all(0.0), + ); + + // Add a Filtering bar + // text: Type to search or create + var textField = TextField( + controller: _textController, + maxLines: 1, + decoration: const InputDecoration( + hintText: 'Type to Search or Create a Repo', + border: OutlineInputBorder(), + ), + ); + + bool canContinue = selectedRepo != null || createRepo; + var columns = Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Choose or create a repository -', + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 16.0), + textField, + const SizedBox(height: 8.0), + Expanded(child: repoBuilder), + const SizedBox(height: 8.0), + Opacity( + opacity: canContinue ? 1.0 : 0.0, + child: GitHostSetupButton( + text: "Next", + onPressed: () async { + if (selectedRepo != null) { + widget.onDone(selectedRepo); + return; + } + + try { + var repoName = _textController.text.trim(); + var repo = await widget.gitHost.createRepo(repoName); + widget.onDone(repo); + return; + } catch (e, stacktrace) { + _handleGitHostException(e, stacktrace); + } + }, + ), + ), + const SizedBox(height: 32.0), + ], + ); + + return SafeArea(child: Center(child: columns)); + } + + Widget _buildRepoTile(GitHostRepo repo) { + final _dateFormat = DateFormat('dd MMM, yyyy'); + + Widget trailing = Container(); + if (repo.updatedAt != null) { + var dateStr = _dateFormat.format(repo.updatedAt); + + var textTheme = Theme.of(context).textTheme; + trailing = Text(dateStr, style: textTheme.caption); + } + + return ListTile( + title: Text(repo.fullName), + trailing: trailing, + selected: repo == selectedRepo, + onTap: () { + setState(() { + selectedRepo = repo; + createRepo = false; + }); + }, + contentPadding: const EdgeInsets.all(0.0), + ); + } + + Widget _buildCreateRepoTile() { + var repoName = _textController.text.trim(); + + return ListTile( + leading: const Icon(Icons.add), + title: Text('Create repo "$repoName"'), + contentPadding: const EdgeInsets.all(0.0), + onTap: () { + setState(() { + createRepo = true; + selectedRepo = null; + }); + }, + selected: createRepo, + ); + } +} diff --git a/lib/setup/screens.dart b/lib/setup/screens.dart index f7afdedd..529994e5 100644 --- a/lib/setup/screens.dart +++ b/lib/setup/screens.dart @@ -9,6 +9,8 @@ import 'package:git_bindings/git_bindings.dart'; import 'package:gitjournal/analytics.dart'; import 'package:gitjournal/apis/githost_factory.dart'; +import 'package:gitjournal/setup/autoconfigure_complete.dart'; +import 'package:gitjournal/setup/repo_selector.dart'; import 'package:gitjournal/state_container.dart'; import 'package:gitjournal/utils.dart'; import 'package:gitjournal/settings.dart'; @@ -49,6 +51,9 @@ class GitHostSetupScreenState extends State { var _keyGenerationChoice = KeyGenerationChoice.Unknown; var _gitHostType = GitHostType.Unknown; + GitHost _gitHost; + GitHostRepo _gitHostRepo; + var _gitCloneUrl = ""; var gitCloneErrorMessage = ""; var publicKey = ""; @@ -156,13 +161,12 @@ class GitHostSetupScreenState extends State { } else if (_pageChoice[1] == PageChoice1.Auto) { return GitHostSetupAutoConfigure( gitHostType: _gitHostType, - onDone: (String gitCloneUrl) { + onDone: (GitHost gitHost) { setState(() { - _gitCloneUrl = gitCloneUrl; + _gitHost = gitHost; _pageCount = pos + 2; _nextPage(); - _startGitClone(context); }); }, ); @@ -225,7 +229,16 @@ class GitHostSetupScreenState extends State { }, ); } else if (_pageChoice[1] == PageChoice1.Auto) { - return GitHostSetupGitClone(errorMessage: gitCloneErrorMessage); + return GitHostSetupRepoSelector( + gitHost: _gitHost, + onDone: (GitHostRepo repo) { + setState(() { + _gitHostRepo = repo; + _pageCount = pos + 2; + _nextPage(); + }); + }, + ); } assert(false); @@ -271,18 +284,30 @@ class GitHostSetupScreenState extends State { }, ); } + } else if (_pageChoice[1] == PageChoice1.Auto) { + return GitHostSetupAutoConfigureComplete( + gitHost: _gitHost, + repo: _gitHostRepo, + onDone: (String gitCloneUrl) { + setState(() { + _gitCloneUrl = gitCloneUrl; + _pageCount = pos + 2; + + _nextPage(); + _startGitClone(context); + }); + }, + ); } } if (pos == 5) { - if (_pageChoice[1] == PageChoice1.Manual) { - return GitHostSetupGitClone(errorMessage: gitCloneErrorMessage); - } + return GitHostSetupGitClone(errorMessage: gitCloneErrorMessage); } assert(_pageChoice[0] != PageChoice0.CustomProvider); - assert(false); + assert(false, "Pos is $pos"); return null; } @@ -450,6 +475,7 @@ class GitHostSetupScreenState extends State { Log.d("Cloning " + _gitCloneUrl); await GitRepo.clone(repoPath, _gitCloneUrl); } on GitException catch (e) { + Log.e(e.toString()); error = e.cause; }