diff --git a/lib/cubit/repo_cubit.dart b/lib/cubit/repo_cubit.dart index 3e49464..ec32b93 100644 --- a/lib/cubit/repo_cubit.dart +++ b/lib/cubit/repo_cubit.dart @@ -1,8 +1,10 @@ import 'package:bloc/bloc.dart'; +import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:equatable/equatable.dart'; import 'package:gitea_client/cubit/repo_event.dart'; import 'package:gitea_client/model/repository.dart'; import 'package:gitea_client/service/gitea_service.dart'; +import 'package:stream_transform/stream_transform.dart'; part 'repo_state.dart'; @@ -12,7 +14,8 @@ class RepoCubit extends Cubit { class RepoBloc extends Bloc { RepoBloc({required this.giteaService}) : super(const RepoState()) { - on(_onRepoFetched); + on(_onRepoFetched, + transformer: (events, mapper) => events.switchMap(mapper),); } final GiteaService giteaService; @@ -31,11 +34,17 @@ class RepoBloc extends Bloc { )); } final repos = await giteaService.getUserRepositories(state.loadedPages+1,100); + final repoList = List.of(state.repos); + repos.forEach((element) { + if(repoList.where((selement) => selement.id == element.id).isEmpty) { + repoList.add(element); + } + }); emit(repos.isEmpty ? state.copyWith(hasReachedMax: true) : state.copyWith( status: RepoStatus.success, - repos: List.of(state.repos)..addAll(repos), + repos: repoList, loadedPages: state.loadedPages+1, hasReachedMax: false, error_message: null, diff --git a/lib/main.dart b/lib/main.dart index bdfde2e..a556c3b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ 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:shared_preferences/shared_preferences.dart'; import 'model/ApiAccess.dart'; @@ -21,7 +22,7 @@ class MyApp extends StatelessWidget { theme: ThemeData( primarySwatch: Colors.lightGreen, ), - home: const LoginPage(title: 'Gitea client'), + home: MyHomePage(), routes: { }, @@ -47,6 +48,47 @@ class MyApp extends StatelessWidget { } } +class MyHomePage extends StatefulWidget { + @override + _MyHomePage createState() => _MyHomePage(); +} + +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) { + await prefs.setBool("autoLogin", true); + setState(() { + loggedIn = true; + _apiAccess = ApiAccess(instance, token); + }); + + print("Auto login"); + } + } + + @override + void initState() { + super.initState(); + _autoLogin(); + } + + @override + Widget build(BuildContext context) { + 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); diff --git a/lib/widget/login_form.dart b/lib/widget/login_form.dart index fa9db06..9b6d1ba 100644 --- a/lib/widget/login_form.dart +++ b/lib/widget/login_form.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; import '../model/ApiAccess.dart'; @@ -17,6 +18,14 @@ class _LoginForm extends State { final instanceController = TextEditingController(); String instance = "gitea.com"; + late SharedPreferences prefs; + + + @override + void initState() { + super.initState(); + } + @override Widget build(BuildContext context) { final media = MediaQuery.of(context).size; diff --git a/lib/widget/login_status.dart b/lib/widget/login_status.dart index ed281d7..a3ea69e 100644 --- a/lib/widget/login_status.dart +++ b/lib/widget/login_status.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../model/ApiAccess.dart'; import '../model/user.dart'; @@ -19,11 +20,15 @@ class _StatefulLoginStatus extends State { @override void initState() { + _initPrefs(); userRequest = AuthenticationChecker(widget.apiAccess).getAuthenticatedUserOrError(); + _autoLogin(); super.initState(); } + late SharedPreferences prefs; + @override Widget build(BuildContext context) { return Scaffold( @@ -79,9 +84,12 @@ class _StatefulLoginStatus extends State { )), ElevatedButton( onPressed: () => { - Navigator.pushNamed(context, "/repolist",arguments: - SavedUser(authedUser: user, apiAccess: widget.apiAccess)) - }, + _saveApiAccess(widget.apiAccess), + Navigator.pushNamed(context, "/repolist", + arguments: SavedUser( + authedUser: user, + apiAccess: widget.apiAccess)) + }, child: const Text("Start using Gitea")) ], )); @@ -95,4 +103,27 @@ class _StatefulLoginStatus extends State { ), )); } + + void _initPrefs() async { + prefs = await SharedPreferences.getInstance(); + } + + void _saveApiAccess(ApiAccess apiAccess) async { + await prefs.setString('instance', apiAccess.instance); + await prefs.setString('token', apiAccess.token); + } + + void _autoLogin() async{ + prefs = await SharedPreferences.getInstance(); + bool? autologin = prefs.getBool("autoLogin"); + if(autologin != null){ + userRequest!.then((user) => { + Navigator.pushNamed(context, "/repolist", + arguments: SavedUser( + authedUser: user, + apiAccess: widget.apiAccess)) + }); + + } + } } diff --git a/lib/widget/repo_list.dart b/lib/widget/repo_list.dart index fac4d8f..e4f35f3 100644 --- a/lib/widget/repo_list.dart +++ b/lib/widget/repo_list.dart @@ -29,13 +29,16 @@ class _ReposListState extends State { return Center(child: Text('failed to fetch $error_message')); case RepoStatus.success: if (state.repos.isEmpty) { - return const Center(child: Text('no posts')); + return const Center(child: Text('no repos')); + } + if(state.repos.length < 5) { + context.read().add(RepoFetched()); } return ListView.builder( itemBuilder: (BuildContext context, int index) { return index >= state.repos.length ? BottomLoader() - : RepoListItem(post: state.repos[index]); + : RepoListItem(repo: state.repos[index]); }, itemCount: state.hasReachedMax ? state.repos.length @@ -83,20 +86,34 @@ class BottomLoader extends StatelessWidget { } class RepoListItem extends StatelessWidget { - const RepoListItem({Key? key, required this.post}) : super(key: key); + const RepoListItem({Key? key, required this.repo}) : super(key: key); - final Repository post; + final Repository repo; @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; + return Material( - child: ListTile( - leading: Text('${post.updatedAt!.toString()}', style: textTheme.caption), - title: Text(post.name), - isThreeLine: true, - subtitle: Text(post.owner.username!), - dense: true, + 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()))), + ), ), ); } diff --git a/lib/widget/repo_list_page.dart b/lib/widget/repo_list_page.dart index c671c81..454ab7d 100644 --- a/lib/widget/repo_list_page.dart +++ b/lib/widget/repo_list_page.dart @@ -9,26 +9,64 @@ import 'package:gitea_client/widget/repo_list.dart'; class RepoListPage extends StatefulWidget { final SavedUser savedUser; - const RepoListPage({Key? key,required this.savedUser}) : super (key: key); + const RepoListPage({Key? key, required this.savedUser}) : super(key: key); @override _RepoListPage createState() => _RepoListPage(); - } class _RepoListPage extends State { + + final GlobalKey _key = GlobalKey(); @override Widget build(BuildContext context) { return Scaffold( + key: _key, appBar: AppBar( - title: const Text("Repositories"), - leading: IconButton(icon: const Icon(Icons.menu),onPressed: ()=> {},), + title: const Text("My Repositories"), + leading: IconButton( + icon: const Icon(Icons.menu), + onPressed: () => _key.currentState!.openDrawer(), + ), + ), + drawer: Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + DrawerHeader( + decoration: BoxDecoration( + color: Colors.green, + ), + child: Row( + + children: [ + Container(padding: EdgeInsets.all(5),child: Image.network(widget.savedUser.authedUser.avatarUrl!,width: 60,)), + Text( + widget.savedUser.authedUser.username!, + style: + TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + ), + ], + ), + ), + Container( + color: Colors.lightGreen[100], + child: const ListTile( + title: Text('My Repositories'), + ), + ), + const ListTile( + title: Text('Explore'), + ), + ], + ), ), body: BlocProvider( - create: (_) => RepoBloc(giteaService: GiteaService(apiAccess: widget.savedUser.apiAccess))..add(RepoFetched()), + create: (_) => RepoBloc( + giteaService: GiteaService(apiAccess: widget.savedUser.apiAccess)) + ..add(RepoFetched()), child: ReposList(), ), ); } - -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index c5dbdd2..11c6298 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -36,6 +36,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "8.0.3" + bloc_concurrency: + dependency: "direct main" + description: + name: bloc_concurrency + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" boolean_selector: dependency: transitive description: @@ -134,6 +141,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" file: dependency: transitive description: @@ -280,6 +294,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.5" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" plugin_platform_interface: dependency: transitive description: @@ -287,6 +329,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" provider: dependency: transitive description: @@ -308,6 +357,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.13" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + shared_preferences_ios: + dependency: transitive + description: + name: shared_preferences_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" sky_engine: dependency: transitive description: flutter @@ -453,6 +558,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.5.2" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b5eb12a..c7cfaba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,8 @@ dependencies: equatable: ^2.0.3 flutter_bloc: ^8.0.1 stream_transform: ^2.0.0 + bloc_concurrency: ^0.2.0 + shared_preferences: ^2.0.13 dev_dependencies: flutter_test: