From 0f052d018c5c2711647e760a5c54eb94f9d5222b Mon Sep 17 00:00:00 2001 From: Bazsalanszky Date: Sun, 15 May 2022 13:17:53 +0200 Subject: [PATCH] Basic Repo overview --- lib/main.dart | 31 ++-- lib/model/File.dart | 89 ++++++++++ lib/model/repouser.dart | 10 ++ lib/service/gitea_service.dart | 12 ++ lib/widget/login_status.dart | 14 +- lib/widget/repo_list.dart | 59 ++++--- lib/widget/repo_list_page.dart | 2 +- lib/widget/repo_overview.dart | 223 ++++++++++++++++++++++++++ linux/flutter/generated_plugins.cmake | 8 + pubspec.lock | 39 +++-- pubspec.yaml | 2 + 11 files changed, 437 insertions(+), 52 deletions(-) create mode 100644 lib/model/File.dart create mode 100644 lib/model/repouser.dart create mode 100644 lib/widget/repo_overview.dart diff --git a/lib/main.dart b/lib/main.dart index a556c3b..57e803b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:gitea_client/model/repouser.dart'; import 'package:gitea_client/model/user.dart'; import 'package:gitea_client/widget/login_form.dart'; import 'package:gitea_client/widget/login_status.dart'; import 'package:gitea_client/widget/repo_list_page.dart'; +import 'package:gitea_client/widget/repo_overview.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'model/ApiAccess.dart'; @@ -23,11 +25,10 @@ class MyApp extends StatelessWidget { primarySwatch: Colors.lightGreen, ), home: MyHomePage(), - routes: { - - }, + routes: {}, onGenerateRoute: (route) { - switch (route.name) { // <- here + switch (route.name) { + // <- here case "/loginstatus": return MaterialPageRoute( settings: const RouteSettings(name: "/parameterpage"), @@ -38,12 +39,18 @@ class MyApp extends StatelessWidget { case "/repolist": return MaterialPageRoute( settings: const RouteSettings(name: "/repolist"), - builder: (context) => RepoListPage(savedUser: route.arguments as SavedUser), + builder: (context) => + RepoListPage(savedUser: route.arguments as SavedUser), ); + case "/repopage": + final repouser = route.arguments as RepoUser; + return MaterialPageRoute( + settings: const RouteSettings(name: "/repopage"), + builder: (context) => + RepoPage(repo: repouser.repository, user: repouser.user)); } return null; }, - ); } } @@ -54,17 +61,15 @@ class MyHomePage extends StatefulWidget { } class _MyHomePage extends State { - late SharedPreferences prefs; ApiAccess? _apiAccess; var loggedIn = false; - void _autoLogin() async { prefs = await SharedPreferences.getInstance(); final token = prefs.getString("token"); final instance = prefs.getString("instance"); - if(token != null && instance != null) { + if (token != null && instance != null) { await prefs.setBool("autoLogin", true); setState(() { loggedIn = true; @@ -83,12 +88,12 @@ class _MyHomePage extends State { @override Widget build(BuildContext context) { - return (loggedIn) ? StatefulLoginStatus(apiAccess: _apiAccess!) : const LoginPage(title: "Login to Gitea"); + return (loggedIn) + ? StatefulLoginStatus(apiAccess: _apiAccess!) + : const LoginPage(title: "Login to Gitea"); } - } - class LoginPage extends StatelessWidget { const LoginPage({Key? key, required this.title}) : super(key: key); @@ -118,4 +123,4 @@ class LoginPage extends StatelessWidget { ), )); } -} \ No newline at end of file +} diff --git a/lib/model/File.dart b/lib/model/File.dart new file mode 100644 index 0000000..15f94f0 --- /dev/null +++ b/lib/model/File.dart @@ -0,0 +1,89 @@ +class RepoFile { + String name; + String path; + String sha; + String type; + int size; + String? encoding; + String? content; + String? target; + String url; + String htmlUrl; + String gitUrl; + String? downloadUrl; + String? submoduleGitUrl; + Links lLinks; + + RepoFile( + {required this.name, + required this.path, + required this.sha, + required this.type, + required this.size, + this.encoding, + this.content, + this.target, + required this.url, + required this.htmlUrl, + required this.gitUrl, + this.downloadUrl, + this.submoduleGitUrl, + required this.lLinks}); + + RepoFile.fromJson(Map json) + : name = json['name'], + path = json['path'], + sha = json['sha'], + type = json['type'], + size = json['size'], + url = json['url'], + htmlUrl = json['html_url'], + gitUrl = json['git_url'], + lLinks = Links.fromJson(json['_links']) { + encoding = json['encoding']; + content = json['content']; + target = json['target']; + downloadUrl = json['download_url']; + submoduleGitUrl = json['submodule_git_url']; + } + + Map toJson() { + final Map data = {}; + data['name'] = name; + data['path'] = path; + data['sha'] = sha; + data['type'] = type; + data['size'] = size; + data['encoding'] = encoding; + data['content'] = content; + data['target'] = target; + data['url'] = url; + data['html_url'] = htmlUrl; + data['git_url'] = gitUrl; + data['download_url'] = downloadUrl; + data['submodule_git_url'] = submoduleGitUrl; + data['_links'] = lLinks.toJson(); + return data; + } +} + +class Links { + String self; + String git; + String html; + + Links({required this.self, required this.git, required this.html}); + + Links.fromJson(Map json) + : self = json['self'], + git = json['git'], + html = json['html']; + + Map toJson() { + final Map data = {}; + data['self'] = self; + data['git'] = git; + data['html'] = html; + return data; + } +} diff --git a/lib/model/repouser.dart b/lib/model/repouser.dart new file mode 100644 index 0000000..0d4d682 --- /dev/null +++ b/lib/model/repouser.dart @@ -0,0 +1,10 @@ + +import 'package:gitea_client/model/repository.dart'; +import 'package:gitea_client/model/user.dart'; + +class RepoUser { + final Repository repository; + final SavedUser user; + + RepoUser(this.repository, this.user); +} \ No newline at end of file diff --git a/lib/service/gitea_service.dart b/lib/service/gitea_service.dart index e4ab4be..088864b 100644 --- a/lib/service/gitea_service.dart +++ b/lib/service/gitea_service.dart @@ -6,6 +6,8 @@ import 'package:gitea_client/model/repository.dart'; import 'package:gitea_client/model/user.dart'; import 'package:http/http.dart' as http; +import '../model/File.dart'; + User _parseAuthenticatedUserResponse(String message){ return User.fromJson(jsonDecode(message)); } @@ -23,6 +25,16 @@ class GiteaService { ); return compute(_parseAuthenticatedUserResponse, response.body); } + + Future getFile(String owner,String repo,String path) async{ + var response = await http.get( + Uri.https(apiAccess.instance, "api/v1/repos/$owner/$repo/contents/$path", { + "token": apiAccess.token, + }), + ); + + return RepoFile.fromJson(jsonDecode(response.body)); + } // Future> getUserRepositories([int page = 1, int limit = 10]) async{ var response = await http.get( diff --git a/lib/widget/login_status.dart b/lib/widget/login_status.dart index a3ea69e..09beed6 100644 --- a/lib/widget/login_status.dart +++ b/lib/widget/login_status.dart @@ -113,17 +113,15 @@ class _StatefulLoginStatus extends State { await prefs.setString('token', apiAccess.token); } - void _autoLogin() async{ + void _autoLogin() async { prefs = await SharedPreferences.getInstance(); bool? autologin = prefs.getBool("autoLogin"); - if(autologin != null){ + if (autologin != null) { userRequest!.then((user) => { - Navigator.pushNamed(context, "/repolist", - arguments: SavedUser( - authedUser: user, - apiAccess: widget.apiAccess)) - }); - + Navigator.of(context).pushReplacementNamed("/repolist", + arguments: + SavedUser(authedUser: user, apiAccess: widget.apiAccess)) + }); } } } diff --git a/lib/widget/repo_list.dart b/lib/widget/repo_list.dart index e4f35f3..36a2903 100644 --- a/lib/widget/repo_list.dart +++ b/lib/widget/repo_list.dart @@ -1,11 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gitea_client/cubit/repo_event.dart'; +import 'package:gitea_client/model/ApiAccess.dart'; import 'package:gitea_client/model/repository.dart'; +import 'package:gitea_client/model/repouser.dart'; +import 'package:gitea_client/model/user.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../cubit/repo_cubit.dart'; class ReposList extends StatefulWidget { + final SavedUser user; + + const ReposList({Key? key, required this.user}) : super(key: key); @override _ReposListState createState() => _ReposListState(); } @@ -31,14 +38,14 @@ class _ReposListState extends State { if (state.repos.isEmpty) { return const Center(child: Text('no repos')); } - if(state.repos.length < 5) { + if (state.repos.length < 5) { context.read().add(RepoFetched()); } return ListView.builder( itemBuilder: (BuildContext context, int index) { return index >= state.repos.length ? BottomLoader() - : RepoListItem(repo: state.repos[index]); + : RepoListItem(repo: state.repos[index],user: widget.user,); }, itemCount: state.hasReachedMax ? state.repos.length @@ -86,10 +93,10 @@ class BottomLoader extends StatelessWidget { } class RepoListItem extends StatelessWidget { - const RepoListItem({Key? key, required this.repo}) : super(key: key); + const RepoListItem({Key? key, required this.repo, required this.user}) : super(key: key); final Repository repo; - + final SavedUser user; @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; @@ -97,24 +104,34 @@ class RepoListItem extends StatelessWidget { return Material( child: Container( color: (repo.private!) ? Colors.yellow[100] : Colors.white, - child: ListTile( - leading: (repo.mirror!) ? const Icon(Icons.amp_stories_outlined) : (repo.archived!) ? const Icon(Icons.archive) : const Icon(Icons.book) , - title: Text('${repo.owner.username}/${repo.name}'), - isThreeLine: true, - subtitle: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - (repo.description != null) ? Text(repo.description!) : Container(), - (repo.mirror!) ? Text("Mirror of ${repo.originalUrl!}") : Container() - ], - ), - dense: true, - onTap: () => Scaffold - .of(context) - .showSnackBar(SnackBar(content: Text(repo.fullName!.toString()))), + child: ListTile( + leading: (repo.private!) + ? const Icon(Icons.lock) + : (repo.mirror!) + ? const Icon(Icons.amp_stories_outlined) + : (repo.archived!) + ? const Icon(Icons.archive) + : const Icon(Icons.book), + title: Text('${repo.owner.username}/${repo.name}'), + isThreeLine: true, + subtitle: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (repo.description != null) + ? Text(repo.description!) + : Container(), + (repo.mirror!) + ? Text("Mirror of ${repo.originalUrl!}") + : Container() + ], + ), + dense: true, + onTap: () => { + Navigator.pushNamed(context, "/repopage",arguments: RepoUser(repo,user)) + }, ), ), ); } -} \ No newline at end of file +} diff --git a/lib/widget/repo_list_page.dart b/lib/widget/repo_list_page.dart index 454ab7d..66ff22b 100644 --- a/lib/widget/repo_list_page.dart +++ b/lib/widget/repo_list_page.dart @@ -65,7 +65,7 @@ class _RepoListPage extends State { create: (_) => RepoBloc( giteaService: GiteaService(apiAccess: widget.savedUser.apiAccess)) ..add(RepoFetched()), - child: ReposList(), + child: ReposList(user: widget.savedUser, ), ), ); } diff --git a/lib/widget/repo_overview.dart b/lib/widget/repo_overview.dart new file mode 100644 index 0000000..d95445b --- /dev/null +++ b/lib/widget/repo_overview.dart @@ -0,0 +1,223 @@ +import 'dart:convert'; + +import 'package:badges/badges.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:gitea_client/model/repository.dart'; +import 'package:gitea_client/model/user.dart'; +import 'package:gitea_client/service/gitea_service.dart'; + +import '../model/File.dart'; + +class RepoPage extends StatefulWidget { + final Repository repo; + final SavedUser user; + + const RepoPage({Key? key, required this.repo, required this.user}) + : super(key: key); + + @override + _RepoPage createState() => _RepoPage(); +} + +class _RepoPage extends State { + int _currentIndex = 0; + + late final List _screens; + + @override + void initState() { + _screens = [ + RepoHome( + repo: widget.repo, + user: widget.user, + ), + RepoFiles( + repo: widget.repo, + user: widget.user, + ), + RepoIssues( + repo: widget.repo, + user: widget.user, + ), + RepoPullRequests( + repo: widget.repo, + user: widget.user, + ) + ]; + } + + void _updateIndex(int value) { + setState(() { + _currentIndex = value; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.repo.name), + ), + body: _screens[_currentIndex], + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.fixed, + currentIndex: _currentIndex, + onTap: _updateIndex, + selectedItemColor: Colors.green[700], + selectedFontSize: 13, + unselectedFontSize: 13, + iconSize: 30, + items: [ + const BottomNavigationBarItem( + label: "Home", + icon: Icon(Icons.home), + ), + const BottomNavigationBarItem( + label: "Files", + icon: Icon(Icons.folder), + ), + if (widget.repo.hasIssues!) + BottomNavigationBarItem( + label: "Issues", + icon: (widget.repo.openIssuesCount! > 0) + ? Badge( + badgeContent: + Text(widget.repo.openIssuesCount!.toString()), + child: const Icon(Icons.error_outline), + ) + : const Icon(Icons.error_outline), + ), + if (widget.repo.hasPullRequests!) + BottomNavigationBarItem( + label: "Pull requests", + icon: (widget.repo.openPrCounter! > 0) + ? Badge( + badgeContent: Text(widget.repo.openPrCounter!.toString()), + child: const Icon(Icons.mediation_rounded), + ) + : const Icon(Icons.mediation_rounded), + ), + ], + ), + ); + } +} + +class RepoHome extends StatefulWidget { + final Repository repo; + final SavedUser user; + + const RepoHome({Key? key, required this.repo, required this.user}) + : super(key: key); + + @override + _RepoHome createState() => _RepoHome(); +} + +class _RepoHome extends State { + @override + Widget build(BuildContext context) { + final media = MediaQuery.of(context).size; + + return Column(children: [ + Text(widget.repo.fullName!, + style: Theme.of(context).textTheme.headline3,), + FutureBuilder( + future: readmeRequest, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Center( + child: Column( + children: [ + Text("No Readme file found!", + style: Theme.of(context).textTheme.headline6), + ], + ), + ); + } else if (snapshot.hasData) { + var file = snapshot.data!; + final content = utf8.decode(base64.decode(file.content!)); + + return SingleChildScrollView(child: Center( + child: SizedBox( + width: (media.width > 600) ? media.width * 0.6 : media.width*0.9, + height: media.height*0.7, + child: Markdown(selectable: true, data: content)), + )); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }) + ]); + } + + Future? readmeRequest; + late final GiteaService giteaService; + + @override + void initState() { + giteaService = GiteaService(apiAccess: widget.user.apiAccess); + readmeRequest = giteaService.getFile( + widget.repo.owner.username!, widget.repo.name, "README.md"); + } +} + +class RepoFiles extends StatefulWidget { + final Repository repo; + final SavedUser user; + + const RepoFiles({Key? key, required this.repo, required this.user}) + : super(key: key); + + @override + _RepoFiles createState() => _RepoFiles(); +} + +class _RepoFiles extends State { + @override + Widget build(BuildContext context) { + // TODO: implement build + throw UnimplementedError(); + } +} + +class RepoIssues extends StatefulWidget { + final Repository repo; + final SavedUser user; + + const RepoIssues({Key? key, required this.repo, required this.user}) + : super(key: key); + + @override + _RepoIssues createState() => _RepoIssues(); +} + +class _RepoIssues extends State { + @override + Widget build(BuildContext context) { + // TODO: implement build + throw UnimplementedError(); + } +} + +class RepoPullRequests extends StatefulWidget { + final Repository repo; + final SavedUser user; + + const RepoPullRequests({Key? key, required this.repo, required this.user}) + : super(key: key); + + @override + _RepoPullRequests createState() => _RepoPullRequests(); +} + +class _RepoPullRequests extends State { + @override + Widget build(BuildContext context) { + // TODO: implement build + throw UnimplementedError(); + } +} diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 1fc8ed3..f16b4c3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST url_launcher_linux ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/pubspec.lock b/pubspec.lock index 11c6298..0bcaa27 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,6 +29,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.2" + badges: + dependency: "direct main" + description: + name: badges + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" bloc: dependency: transitive description: @@ -98,7 +105,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" convert: dependency: transitive description: @@ -140,7 +147,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" ffi: dependency: transitive description: @@ -179,6 +186,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_markdown: + dependency: "direct main" + description: + name: flutter_markdown + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.10" flutter_test: dependency: "direct dev" description: flutter @@ -223,7 +237,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" json_annotation: dependency: transitive description: @@ -252,6 +266,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + markdown: + dependency: transitive + description: + name: markdown + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" matcher: dependency: transitive description: @@ -265,7 +286,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" meta: dependency: transitive description: @@ -293,7 +314,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" path_provider_linux: dependency: transitive description: @@ -438,7 +459,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -480,7 +501,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.9" typed_data: dependency: transitive description: @@ -550,7 +571,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" watcher: dependency: transitive description: @@ -580,5 +601,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.16.2 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=2.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index c7cfaba..a1ef526 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,8 @@ dependencies: stream_transform: ^2.0.0 bloc_concurrency: ^0.2.0 shared_preferences: ^2.0.13 + badges: ^2.0.2 + flutter_markdown: ^0.6.10 dev_dependencies: flutter_test: