From ae32e7c0c45ac0832aab89c0c007539bb78845a6 Mon Sep 17 00:00:00 2001 From: dbilgin Date: Mon, 8 Apr 2024 03:19:15 +0200 Subject: [PATCH 1/5] added google sign in --- android/app/build.gradle | 21 +++++- android/app/google-services.json | 56 ++++++++++++++++ android/settings.gradle | 2 +- assets/google_drive.png | Bin 0 -> 3067 bytes lib/cubit/github_cubit.dart | 28 +++++--- lib/cubit/google_drive_cubit.dart | 63 ++++++++++++++++++ lib/cubit/models/google_sign_in_result.dart | 44 ++++++++++++ .../models/remote_connection_result.dart | 9 ++- lib/main.dart | 24 +++++-- lib/models/google_drive_state.dart | 54 +++++++++++++++ lib/repository/github_repository.dart | 16 +---- lib/repository/google_drive_repository.dart | 41 ++++++++++++ lib/screens/google_drive_page.dart | 51 ++++++++++++++ lib/screens/home_page.dart | 13 +++- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 48 +++++++++++++ pubspec.yaml | 2 + 17 files changed, 438 insertions(+), 36 deletions(-) create mode 100644 android/app/google-services.json create mode 100644 assets/google_drive.png create mode 100644 lib/cubit/google_drive_cubit.dart create mode 100644 lib/cubit/models/google_sign_in_result.dart create mode 100644 lib/models/google_drive_state.dart create mode 100644 lib/repository/google_drive_repository.dart create mode 100644 lib/screens/google_drive_page.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index e94ea37..cee5ef3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -4,6 +4,14 @@ plugins { id "dev.flutter.flutter-gradle-plugin" } +def keyProperties = new Properties() +keyProperties.load(new FileInputStream(file('key.properties'))) + +def debugStoreFile = keyProperties.getProperty('DEBUG_STORE_FILE') +def debugStorePassword = keyProperties.getProperty('DEBUG_STORE_PASSWORD') +def debugKeyAlias = keyProperties.getProperty('DEBUG_KEY_ALIAS') +def debugKeyPassword = keyProperties.getProperty('DEBUG_KEY_PASSWORD') + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -41,14 +49,21 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.omedacore.notelytask" - // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName + multiDexEnabled true + } + + signingConfigs { + debug { + storeFile file(debugStoreFile) + storePassword debugStorePassword + keyAlias debugKeyAlias + keyPassword debugKeyPassword + } } buildTypes { diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..3c7e75d --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,56 @@ +{ + "project_info": { + "project_number": "731300865453", + "firebase_url": "https://notelytask.firebaseio.com", + "project_id": "notelytask", + "storage_bucket": "notelytask.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:731300865453:android:9a6638f59f98d66773ad24", + "android_client_info": { + "package_name": "com.omedacore.notelytask" + } + }, + "oauth_client": [ + { + "client_id": "731300865453-l079ctbqc4tbhtkmgm56atjo3m610lf7.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.omedacore.notelytask", + "certificate_hash": "a673aa29997e33ad1024aacbe8d19ab4e62a3d65" + } + }, + { + "client_id": "731300865453-lf1glbqkkmbld8qvcvl3s5kqohj11m3d.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.omedacore.notelytask", + "certificate_hash": "8d7aef42c68464a9bd641fad8579331671f14bde" + } + }, + { + "client_id": "731300865453-ad51mhq87bsg9ah5305qsu0fd7no815u.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAsA_4g7Bg4niV4pGbwSp0jKQvYjr-Y_nc" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "731300865453-ad51mhq87bsg9ah5305qsu0fd7no815u.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index 1d6d19b..1744d99 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -20,7 +20,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "7.3.0" apply false - id "org.jetbrains.kotlin.android" version "1.7.10" apply false + id "org.jetbrains.kotlin.android" version "1.9.23" apply false } include ":app" diff --git a/assets/google_drive.png b/assets/google_drive.png new file mode 100644 index 0000000000000000000000000000000000000000..71158dd1c3e8270380a1746bbfa51a9c9e82a928 GIT binary patch literal 3067 zcmeHJ`8N~{7oHjG%uph`v1ZA>g)C*87-VG0sCk(z$v#DlWH)8sUnYA*gvnO+$z)## zuk0a1Mu>_`sQJ8q#&^zp&w1{1?zzto_dMtM;U?Hvn{h&fAOHY>6Jc&@d&;enSI)Jg}PJsXbl;>Xn0eJ-i005}Q#>yUkx@7!U|2gpg;DCS_ z`|xRA*Me-V?Etd3Zacer-1YMD4G0VlaTEH%z~E?#Ad#KlF=w7~e*3KYD-2pt#`e#y zgu<#t+@EqF`JCFuk)goYvG*W-R`--sw2*^{E` zEjo6OKgy+ShQEzR{54%#p@!PdGQt745xZ4yWwP0{gk}te$TL#?{FbB0Dz5-Fg3Er73}1LZR|Zn zx|VY@lfZsMb(&Pj^!zGuM*efSgi(q3QWRfe)qRUq`D^Aw&*Ql+lwM31N{LzN$wnL8{bczn+kaWl(EjJv)7}SLLNQ8}ci|``n zSJseNh9@R&AhIR@*OMhIqbh2hy~!-3h?Rh)3pdcB{SlCxnr`cHqM#@0`z=n!*vL5l z6=*x@EWj>ozcUUaEbZOHFHhGFGCatPE?jFqPg5sNmF7p?zs(3J2PvGdOc03Tptm8n zilbVguQJ%T@17Jn&_ExLC_apknn#I9g zv|;wUOBTSNstkAkO75+hT`j<0eI-)dQJy6~DypY%);ykYs-(OS9&ZN4{+1N9)0 zu>whpH*Rllm}xm0t@PrJi?0iGylTSFy{MCI-lpBWM;-_=*02%ic#-^(4{hAWOgR8` z#g!qi@sri$ia*M|L$-k)WkWM^Uj}oEoJel;1a%xjr!_dn$@JGUCk85=^~ZkBa>q2$6z%r-aM16v2|d{Q7534vaZK6Q zJZjlqfVhfYYmwXVXXZ4o=*6V$d|23X2DCh?Uhxi7n7(pdn4zL-w)*NeDK#H$u-4KW zaQCHb%hB2nzO=!wH=7F%6J{dt2(r`+4gqsy{Q!M8@N9Mghh+|iZ0&2ks;F~WV5RgO zUQIEZZ)Bm${#5mfnE}@RSdxP5(@ z%pS!O>>GQgK!1(mJKw-R^eQw=k(YH#R-I$EAq**9UE5EnHdOH_Ef#M%@fk>KpcD=I zqYZv@mI$jg=e=!K?}BR$mjJb1uX!6xg03SZQ?xXhPKNv{+ZXhf(4*vNBO6LhT2-_x zOurcj2{Td0eQ(y~_eaJ`vUSkA5{)ns^6hylqPoPVSN7?+V*B#+t$&k~pGc2t2FLJR%&Uqh`UJTWOBhx1Z|@|?uvyYOi#K@7Y?q29K77c2LeX^&m6<)2Cjut&rIp}I^EG8-MbZ? zw+bm+0r2XV*xK44jobOUMXIq6@oudtB83Q~ZY%rz z#~#a2^Kdo-7P78vh!%)*m^7l*@_dUyx+Ue#arLa%XCu2TG5|6Ty-CY(CSEJe)VmA! zJYV+-jY%fu3i>?Z(ZphS^+vNT`i750-(RAQ#g8o8JG||&-j4S{$!zXEZsiSPfm@pS z`UB0bxaLS$U55URjCMy`j;3)}{G6K19oZ~@(HQu$}CWhBn(66yT8Zz47har zd=eULG3~_-aY`i!eHW~< { final accessToken = state.accessToken; if (accessToken == null) { reset(shouldError: true); - return RemoteConnectionResult(); + return const RemoteConnectionResult(); } emit(state.copyWith(loading: true, error: false)); @@ -107,7 +107,7 @@ class GithubCubit extends HydratedCubit { final content = existingFile?.content; if (keepLocal || existingFile?.sha == null || content == null) { - return RemoteConnectionResult(shouldCreateRemote: true); + return const RemoteConnectionResult(shouldCreateRemote: true); } final isEncryptedString = isEncrypted(content); @@ -115,13 +115,13 @@ class GithubCubit extends HydratedCubit { final encryptionKey = await enterEncryptionKeyDialog(); if (encryptionKey == null) { reset(shouldError: true); - return RemoteConnectionResult(); + return const RemoteConnectionResult(); } final decrypted = decrypt(content, encryptionKey); if (decrypted == null) { reset(shouldError: true); - return RemoteConnectionResult(); + return const RemoteConnectionResult(); } emit(state.copyWith(loading: false)); @@ -132,12 +132,6 @@ class GithubCubit extends HydratedCubit { return RemoteConnectionResult(content: content); } - bool isLoggedIn() { - final ownerRepo = state.ownerRepo; - final accessToken = state.accessToken; - return ownerRepo != null && accessToken != null; - } - Future uploadNewFile( String safeFileName, Uint8List data, @@ -221,6 +215,12 @@ class GithubCubit extends HydratedCubit { emit(state.copyWith(loading: false)); } + bool isLoggedIn() { + final ownerRepo = state.ownerRepo; + final accessToken = state.accessToken; + return ownerRepo != null && accessToken != null; + } + void reset({bool shouldError = false}) { emit(const GithubState()); emit(state.copyWith( @@ -236,7 +236,13 @@ class GithubCubit extends HydratedCubit { } Future launchLogin() async { - var loginResult = await githubRepository.initialLogin(); + reset(); + final loginResult = await githubRepository.initialLogin(); + if (loginResult == null) { + reset(shouldError: true); + return; + } + emit( state.copyWith( deviceCode: loginResult.deviceCode, diff --git a/lib/cubit/google_drive_cubit.dart b/lib/cubit/google_drive_cubit.dart new file mode 100644 index 0000000..775838d --- /dev/null +++ b/lib/cubit/google_drive_cubit.dart @@ -0,0 +1,63 @@ +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:notelytask/models/google_drive_state.dart'; +import 'package:notelytask/repository/google_drive_repository.dart'; + +class GoogleDriveCubit extends HydratedCubit { + GoogleDriveCubit({ + required this.googleDriveRepository, + }) : super(const GoogleDriveState()); + final GoogleDriveRepository googleDriveRepository; + + Future getTokens() async { + reset(); + + final signInData = await googleDriveRepository.signIn(); + if (signInData == null) { + reset(shouldError: true); + return false; + } + + final accessToken = signInData.accessToken; + final idToken = signInData.idToken; + + emit( + state.copyWith( + accessToken: accessToken, + idToken: idToken, + ), + ); + return true; + } + + bool isLoggedIn() { + final idToken = state.idToken; + final accessToken = state.accessToken; + return idToken != null && accessToken != null; + } + + void reset({bool shouldError = false}) { + emit(const GoogleDriveState()); + emit( + state.copyWith( + loading: false, + error: shouldError, + accessToken: null, + idToken: null, + ), + ); + } + + void invalidateError() { + emit(state.copyWith(error: false)); + } + + @override + GoogleDriveState fromJson(Map json) { + return GoogleDriveState.fromJson(json); + } + + @override + Map toJson(GoogleDriveState state) { + return state.toJson(); + } +} diff --git a/lib/cubit/models/google_sign_in_result.dart b/lib/cubit/models/google_sign_in_result.dart new file mode 100644 index 0000000..a4421fa --- /dev/null +++ b/lib/cubit/models/google_sign_in_result.dart @@ -0,0 +1,44 @@ +import 'dart:convert'; + +import 'package:equatable/equatable.dart'; + +class GoogleSignInResult extends Equatable { + final String accessToken; + final String idToken; + const GoogleSignInResult({ + required this.accessToken, + required this.idToken, + }); + + GoogleSignInResult copyWith({ + String? accessToken, + String? idToken, + }) { + return GoogleSignInResult( + accessToken: accessToken ?? this.accessToken, + idToken: idToken ?? this.idToken, + ); + } + + Map toMap() { + return { + 'accessToken': accessToken, + 'idToken': idToken, + }; + } + + factory GoogleSignInResult.fromMap(Map map) { + return GoogleSignInResult( + accessToken: map['accessToken'] as String, + idToken: map['idToken'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory GoogleSignInResult.fromJson(String source) => + GoogleSignInResult.fromMap(json.decode(source) as Map); + + @override + List get props => [accessToken, idToken]; +} diff --git a/lib/cubit/models/remote_connection_result.dart b/lib/cubit/models/remote_connection_result.dart index ff8c935..bb39f47 100644 --- a/lib/cubit/models/remote_connection_result.dart +++ b/lib/cubit/models/remote_connection_result.dart @@ -1,9 +1,14 @@ -class RemoteConnectionResult { +import 'package:equatable/equatable.dart'; + +class RemoteConnectionResult extends Equatable { final String? content; final bool shouldCreateRemote; - RemoteConnectionResult({ + const RemoteConnectionResult({ this.content, this.shouldCreateRemote = false, }); + + @override + List get props => [content, shouldCreateRemote]; } diff --git a/lib/main.dart b/lib/main.dart index 62c0f3c..0e9ca6f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,10 +2,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:notelytask/cubit/github_cubit.dart'; +import 'package:notelytask/cubit/google_drive_cubit.dart'; import 'package:notelytask/cubit/settings_cubit.dart'; import 'package:notelytask/repository/github_repository.dart'; +import 'package:notelytask/repository/google_drive_repository.dart'; import 'package:notelytask/screens/details_page.dart'; import 'package:notelytask/screens/github_page.dart'; +import 'package:notelytask/screens/google_drive_page.dart'; import 'package:notelytask/screens/home_page.dart'; import 'package:notelytask/cubit/notes_cubit.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; @@ -49,11 +52,23 @@ class App extends StatelessWidget { RepositoryProvider( create: (context) => GithubRepository(), ), - ], - child: BlocProvider( - create: (context) => GithubCubit( - githubRepository: context.read(), + RepositoryProvider( + create: (context) => GoogleDriveRepository(), ), + ], + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => GithubCubit( + githubRepository: context.read(), + ), + ), + BlocProvider( + create: (context) => GoogleDriveCubit( + googleDriveRepository: context.read(), + ), + ), + ], child: MultiBlocProvider( providers: [ BlocProvider(create: (context) => SettingsCubit()), @@ -76,6 +91,7 @@ class App extends StatelessWidget { '/': (context) => const HomePage(), '/deleted_list': (context) => const DeletedListPage(), '/github': (context) => GithubPage(code: ghUserCode), + '/google_drive': (context) => const GoogleDrivePage(), '/details': (context) => Scaffold( body: DetailsPage( note: diff --git a/lib/models/google_drive_state.dart b/lib/models/google_drive_state.dart new file mode 100644 index 0000000..197dbca --- /dev/null +++ b/lib/models/google_drive_state.dart @@ -0,0 +1,54 @@ +import 'package:equatable/equatable.dart'; + +class GoogleDriveState extends Equatable { + final bool loading; + final bool error; + + final String? accessToken; + final String? idToken; + + const GoogleDriveState({ + this.loading = false, + this.error = false, + this.accessToken, + this.idToken, + }); + + factory GoogleDriveState.fromJson(Map json) { + return GoogleDriveState( + loading: json['loading'] as bool? ?? false, + error: json['error'] as bool? ?? false, + accessToken: json['accessToken'] as String?, + idToken: json['idToken'] as String?, + ); + } + + Map toJson() => { + 'loading': loading, + 'error': error, + 'accessToken': accessToken, + 'idToken': idToken, + }; + + GoogleDriveState copyWith({ + bool? loading, + bool? error, + String? accessToken, + String? idToken, + }) { + return GoogleDriveState( + loading: loading ?? this.loading, + error: error ?? this.error, + accessToken: accessToken ?? this.accessToken, + idToken: idToken ?? this.idToken, + ); + } + + @override + List get props => [ + loading, + error, + accessToken, + idToken, + ]; +} diff --git a/lib/repository/github_repository.dart b/lib/repository/github_repository.dart index fe37076..779ee6c 100644 --- a/lib/repository/github_repository.dart +++ b/lib/repository/github_repository.dart @@ -208,7 +208,7 @@ class GithubRepository { } } - Future initialLogin() async { + Future initialLogin() async { try { final url = Uri.https( 'github.com', @@ -236,20 +236,10 @@ class GithubRepository { expiresIn: expiresIn, ); } else { - return const GithubState( - deviceCode: null, - userCode: null, - verificationUri: null, - expiresIn: null, - ); + return null; } } catch (e) { - return const GithubState( - deviceCode: null, - userCode: null, - verificationUri: null, - expiresIn: null, - ); + return null; } } diff --git a/lib/repository/google_drive_repository.dart b/lib/repository/google_drive_repository.dart new file mode 100644 index 0000000..544637c --- /dev/null +++ b/lib/repository/google_drive_repository.dart @@ -0,0 +1,41 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:notelytask/cubit/models/google_sign_in_result.dart'; + +class GoogleDriveRepository { + Future signIn() async { + try { + const List scopes = [ + 'https://www.googleapis.com/auth/drive.appdata', + 'https://www.googleapis.com/auth/drive.file', + ]; + + GoogleSignIn googleSignIn = GoogleSignIn( + clientId: dotenv.env['GOOGLE_CLIENT_ID'], + scopes: scopes, + ); + final account = (await googleSignIn.signInSilently()) ?? + (await googleSignIn.signIn()); + + bool isAuthorized = account != null; + if (kIsWeb && account != null) { + isAuthorized = await googleSignIn.canAccessScopes(scopes); + } + + if (!isAuthorized) { + return null; + } + + final auth = await account?.authentication; + final accessToken = auth?.accessToken; + final idToken = auth?.idToken; + + return accessToken != null && idToken != null + ? GoogleSignInResult(accessToken: accessToken, idToken: idToken) + : null; + } catch (e) { + return null; + } + } +} diff --git a/lib/screens/google_drive_page.dart b/lib/screens/google_drive_page.dart new file mode 100644 index 0000000..5fb49db --- /dev/null +++ b/lib/screens/google_drive_page.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:notelytask/cubit/google_drive_cubit.dart'; + +class GoogleDrivePage extends StatefulWidget { + const GoogleDrivePage({super.key}); + + @override + State createState() => _GoogleDrivePageState(); +} + +class _GoogleDrivePageState extends State { + void _connectGoogleDrive() async { + await context.read().getTokens(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'Google Drive', + style: TextStyle(color: Colors.white), + ), + iconTheme: const IconThemeData( + color: Colors.white, + ), + backgroundColor: Theme.of(context).colorScheme.primary, + ), + body: Padding( + padding: const EdgeInsets.all(24.0), + child: SizedBox( + width: double.infinity, + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + runAlignment: WrapAlignment.center, + direction: Axis.vertical, + runSpacing: 24.0, + spacing: 12.0, + children: [ + ElevatedButton( + onPressed: _connectGoogleDrive, + child: const Text('Connect to Google Drive'), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/home_page.dart b/lib/screens/home_page.dart index a600e69..ff5d073 100644 --- a/lib/screens/home_page.dart +++ b/lib/screens/home_page.dart @@ -33,10 +33,14 @@ class _HomePageState extends State { NativeService.updateNotes(context, args); } - void _navigateToLogin() { + void _navigateToGithubLogin() { getIt().pushNamed('/github'); } + void _navigateToGDriveLogin() { + getIt().pushNamed('/google_drive'); + } + void _navigateToDeletedList() { context.read().setSelectedNoteId(null); getIt().pushNamed('/deleted_list'); @@ -61,7 +65,12 @@ class _HomePageState extends State { IconButton( icon: Image.asset('assets/github.png'), tooltip: 'Github Integration', - onPressed: _navigateToLogin, + onPressed: _navigateToGithubLogin, + ), + IconButton( + icon: Image.asset('assets/google_drive.png'), + tooltip: 'Google Drive Integration', + onPressed: _navigateToGDriveLogin, ), ], bottom: const PreferredSize( diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 0100366..6aed8b1 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import audio_session +import google_sign_in_ios import just_audio import package_info_plus import path_provider_foundation @@ -18,6 +19,7 @@ import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 755ffcf..2443eef 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -560,6 +560,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "9482364c9f8b7bd36902572ebc3a7c2b5c8ee57a9c93e6eb5099c1a9ec5265d8" + url: "https://pub.dev" + source: hosted + version: "0.3.1+1" + google_sign_in: + dependency: "direct main" + description: + name: google_sign_in + sha256: "0b8787cb9c1a68ad398e8010e8c8766bfa33556d2ab97c439fb4137756d7308f" + url: "https://pub.dev" + source: hosted + version: "6.2.1" + google_sign_in_android: + dependency: transitive + description: + name: google_sign_in_android + sha256: "38cef11ed22fc0c9bfa7b00bf7f2d91012138f5613089522917fd26c853be93e" + url: "https://pub.dev" + source: hosted + version: "6.1.22" + google_sign_in_ios: + dependency: transitive + description: + name: google_sign_in_ios + sha256: "1e0d4fde6cc07a8ff423f6bc931e83a74163d6af702004bacaee752649fdd2e7" + url: "https://pub.dev" + source: hosted + version: "5.7.5" + google_sign_in_platform_interface: + dependency: transitive + description: + name: google_sign_in_platform_interface + sha256: "1f6e5787d7a120cc0359ddf315c92309069171306242e181c09472d1b00a2971" + url: "https://pub.dev" + source: hosted + version: "2.4.5" + google_sign_in_web: + dependency: transitive + description: + name: google_sign_in_web + sha256: fc0f14ed45ea616a6cfb4d1c7534c2221b7092cc4f29a709f0c3053cc3e821bd + url: "https://pub.dev" + source: hosted + version: "0.12.4" graphs: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index aed1b37..97e71c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: pinput: ^4.0.0 flutter_markdown: ^0.6.22 flutter_widget_from_html: ^0.14.11 + google_sign_in: ^6.2.1 dev_dependencies: flutter_test: @@ -50,6 +51,7 @@ flutter: - assets/icon.png - assets/foreground.png - assets/github.png + - assets/google_drive.png - assets/.env flutter_icons: From 4cd0e29f2855c122ec9db0a0335bd8c4ddd173fe Mon Sep 17 00:00:00 2001 From: dbilgin Date: Mon, 8 Apr 2024 03:24:44 +0200 Subject: [PATCH 2/5] remove unnecessary --- .gitignore | 2 ++ android/app/google-services.json | 56 -------------------------------- 2 files changed, 2 insertions(+), 56 deletions(-) delete mode 100644 android/app/google-services.json diff --git a/.gitignore b/.gitignore index ea515f5..fe2cb02 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,5 @@ app.*.map.json /android/key.properties *.keystore .env + +google-services.json \ No newline at end of file diff --git a/android/app/google-services.json b/android/app/google-services.json deleted file mode 100644 index 3c7e75d..0000000 --- a/android/app/google-services.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "project_info": { - "project_number": "731300865453", - "firebase_url": "https://notelytask.firebaseio.com", - "project_id": "notelytask", - "storage_bucket": "notelytask.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:731300865453:android:9a6638f59f98d66773ad24", - "android_client_info": { - "package_name": "com.omedacore.notelytask" - } - }, - "oauth_client": [ - { - "client_id": "731300865453-l079ctbqc4tbhtkmgm56atjo3m610lf7.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "com.omedacore.notelytask", - "certificate_hash": "a673aa29997e33ad1024aacbe8d19ab4e62a3d65" - } - }, - { - "client_id": "731300865453-lf1glbqkkmbld8qvcvl3s5kqohj11m3d.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "com.omedacore.notelytask", - "certificate_hash": "8d7aef42c68464a9bd641fad8579331671f14bde" - } - }, - { - "client_id": "731300865453-ad51mhq87bsg9ah5305qsu0fd7no815u.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyAsA_4g7Bg4niV4pGbwSp0jKQvYjr-Y_nc" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "731300865453-ad51mhq87bsg9ah5305qsu0fd7no815u.apps.googleusercontent.com", - "client_type": 3 - } - ] - } - } - } - ], - "configuration_version": "1" -} \ No newline at end of file From e9afc8e60738d40c0c5a525cb1117fac45e7b48c Mon Sep 17 00:00:00 2001 From: dbilgin Date: Thu, 11 Apr 2024 05:34:05 +0200 Subject: [PATCH 3/5] sign in sign out for google and loading sync with notes --- lib/cubit/github_cubit.dart | 23 +++--- lib/cubit/google_drive_cubit.dart | 15 +++- lib/cubit/notes_cubit.dart | 44 +++++++---- lib/main.dart | 1 + lib/models/github_state.dart | 6 ++ lib/models/google_drive_state.dart | 6 ++ lib/repository/google_drive_repository.dart | 22 ++++-- lib/screens/deleted_list_page.dart | 4 +- lib/screens/details_page.dart | 4 +- lib/screens/github_page.dart | 11 ++- lib/screens/google_drive_page.dart | 83 ++++++++++++++++----- lib/screens/home_page.dart | 4 +- lib/widgets/github_loader.dart | 22 ------ lib/widgets/state_loader.dart | 24 ++++++ 14 files changed, 181 insertions(+), 88 deletions(-) delete mode 100644 lib/widgets/github_loader.dart create mode 100644 lib/widgets/state_loader.dart diff --git a/lib/cubit/github_cubit.dart b/lib/cubit/github_cubit.dart index 45433e4..a6b48a6 100644 --- a/lib/cubit/github_cubit.dart +++ b/lib/cubit/github_cubit.dart @@ -187,7 +187,7 @@ class GithubCubit extends HydratedCubit { final ownerRepo = state.ownerRepo; final sha = state.sha; final accessToken = state.accessToken; - if (!isLoggedIn() || ownerRepo == null || accessToken == null) { + if (!state.isLoggedIn() || ownerRepo == null || accessToken == null) { return; } emit(state.copyWith(loading: true)); @@ -215,20 +215,15 @@ class GithubCubit extends HydratedCubit { emit(state.copyWith(loading: false)); } - bool isLoggedIn() { - final ownerRepo = state.ownerRepo; - final accessToken = state.accessToken; - return ownerRepo != null && accessToken != null; - } - void reset({bool shouldError = false}) { - emit(const GithubState()); - emit(state.copyWith( - loading: false, - ownerRepo: null, - error: shouldError, - accessToken: null, - )); + emit( + const GithubState().copyWith( + loading: false, + ownerRepo: null, + error: shouldError, + accessToken: null, + ), + ); } void invalidateError() { diff --git a/lib/cubit/google_drive_cubit.dart b/lib/cubit/google_drive_cubit.dart index 775838d..802b33c 100644 --- a/lib/cubit/google_drive_cubit.dart +++ b/lib/cubit/google_drive_cubit.dart @@ -10,6 +10,7 @@ class GoogleDriveCubit extends HydratedCubit { Future getTokens() async { reset(); + emit(state.copyWith(loading: true)); final signInData = await googleDriveRepository.signIn(); if (signInData == null) { @@ -26,13 +27,19 @@ class GoogleDriveCubit extends HydratedCubit { idToken: idToken, ), ); + emit(state.copyWith(loading: false)); return true; } - bool isLoggedIn() { - final idToken = state.idToken; - final accessToken = state.accessToken; - return idToken != null && accessToken != null; + Future signOut() async { + emit(state.copyWith(loading: true)); + final signedOut = await googleDriveRepository.signOut(); + if (signedOut) { + reset(); + } + + emit(state.copyWith(loading: false)); + return signedOut; } void reset({bool shouldError = false}) { diff --git a/lib/cubit/notes_cubit.dart b/lib/cubit/notes_cubit.dart index cd0ddfb..bea7a66 100644 --- a/lib/cubit/notes_cubit.dart +++ b/lib/cubit/notes_cubit.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter/widgets.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:notelytask/cubit/github_cubit.dart'; +import 'package:notelytask/cubit/google_drive_cubit.dart'; import 'package:notelytask/models/file_data.dart'; import 'package:notelytask/models/note.dart'; import 'package:notelytask/models/notes_state.dart'; @@ -13,8 +14,10 @@ import 'package:notelytask/utils.dart'; class NotesCubit extends HydratedCubit { NotesCubit({ required this.githubCubit, + required this.googleDriveCubit, }) : super(const NotesState()); final GithubCubit githubCubit; + final GoogleDriveCubit googleDriveCubit; @override void onChange(Change change) { @@ -29,7 +32,7 @@ class NotesCubit extends HydratedCubit { Future createOrUpdateRemoteNotes({ bool shouldResetIfError = true, }) async { - if (!githubCubit.isLoggedIn()) { + if (!githubCubit.state.isLoggedIn()) { return; } @@ -81,16 +84,25 @@ class NotesCubit extends HydratedCubit { } bool isLoggedIn() { - return githubCubit.isLoggedIn(); + return githubCubit.state.isLoggedIn() || + googleDriveCubit.state.isLoggedIn(); } - Future setRemoteConnection( - String ownerRepo, - bool keepLocal, - Future Function() enterEncryptionKeyDialog, - ) async { + Future setRemoteConnection({ + required bool keepLocal, + required Future Function() enterEncryptionKeyDialog, + String? ownerRepo, + }) async { + // final connectionResult = ownerRepo != null + // ? await githubCubit.setRepoUrl( + // ownerRepo, + // keepLocal, + // enterEncryptionKeyDialog, + // ) + // : await googleDriveCubit.getNotesFile(); + final connectionResult = await githubCubit.setRepoUrl( - ownerRepo, + ownerRepo!, keepLocal, enterEncryptionKeyDialog, ); @@ -112,10 +124,15 @@ class NotesCubit extends HydratedCubit { void reset({ bool shouldError = false, }) { - githubCubit.reset(shouldError: shouldError); - final newState = NotesState( + if (githubCubit.state.isLoggedIn()) { + githubCubit.reset(shouldError: shouldError); + } else { + googleDriveCubit.reset(shouldError: shouldError); + } + + const newState = NotesState( encryptionKey: null, - notes: state.notes, + notes: [], ); emit(newState); } @@ -124,7 +141,7 @@ class NotesCubit extends HydratedCubit { required BuildContext context, String? redirectNoteId, }) async { - if (!githubCubit.isLoggedIn()) { + if (!githubCubit.state.isLoggedIn()) { return; } @@ -144,13 +161,11 @@ class NotesCubit extends HydratedCubit { ); if (pinResult == null) { reset(shouldError: true); - emit(const NotesState()); return; } if (!context.mounted) { reset(shouldError: true); - emit(const NotesState()); return; } @@ -163,7 +178,6 @@ class NotesCubit extends HydratedCubit { if (notesString == null) { reset(shouldError: true); - emit(const NotesState()); } else { final finalContent = json.decode(notesString); final list = fromJson(finalContent); diff --git a/lib/main.dart b/lib/main.dart index 0e9ca6f..e6de76c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -75,6 +75,7 @@ class App extends StatelessWidget { BlocProvider( create: (context) => NotesCubit( githubCubit: context.read(), + googleDriveCubit: context.read(), ), ), ], diff --git a/lib/models/github_state.dart b/lib/models/github_state.dart index 1bd1d40..e9ce98d 100644 --- a/lib/models/github_state.dart +++ b/lib/models/github_state.dart @@ -25,6 +25,12 @@ class GithubState extends Equatable { this.expiresIn, }); + bool isLoggedIn() { + final ownerRepo = this.ownerRepo; + final accessToken = this.accessToken; + return ownerRepo != null && accessToken != null; + } + factory GithubState.fromJson(Map json) { return GithubState( loading: json['loading'] as bool? ?? false, diff --git a/lib/models/google_drive_state.dart b/lib/models/google_drive_state.dart index 197dbca..af748e0 100644 --- a/lib/models/google_drive_state.dart +++ b/lib/models/google_drive_state.dart @@ -14,6 +14,12 @@ class GoogleDriveState extends Equatable { this.idToken, }); + bool isLoggedIn() { + final idToken = this.idToken; + final accessToken = this.accessToken; + return idToken != null && accessToken != null; + } + factory GoogleDriveState.fromJson(Map json) { return GoogleDriveState( loading: json['loading'] as bool? ?? false, diff --git a/lib/repository/google_drive_repository.dart b/lib/repository/google_drive_repository.dart index 544637c..15606c4 100644 --- a/lib/repository/google_drive_repository.dart +++ b/lib/repository/google_drive_repository.dart @@ -4,13 +4,12 @@ import 'package:google_sign_in/google_sign_in.dart'; import 'package:notelytask/cubit/models/google_sign_in_result.dart'; class GoogleDriveRepository { + final List scopes = [ + 'https://www.googleapis.com/auth/drive.appdata', + 'https://www.googleapis.com/auth/drive.file', + ]; Future signIn() async { try { - const List scopes = [ - 'https://www.googleapis.com/auth/drive.appdata', - 'https://www.googleapis.com/auth/drive.file', - ]; - GoogleSignIn googleSignIn = GoogleSignIn( clientId: dotenv.env['GOOGLE_CLIENT_ID'], scopes: scopes, @@ -38,4 +37,17 @@ class GoogleDriveRepository { return null; } } + + Future signOut() async { + try { + GoogleSignIn googleSignIn = GoogleSignIn( + clientId: dotenv.env['GOOGLE_CLIENT_ID'], + scopes: scopes, + ); + await googleSignIn.disconnect(); + return true; + } catch (e) { + return false; + } + } } diff --git a/lib/screens/deleted_list_page.dart b/lib/screens/deleted_list_page.dart index a5f47f6..58bef41 100644 --- a/lib/screens/deleted_list_page.dart +++ b/lib/screens/deleted_list_page.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:notelytask/cubit/notes_cubit.dart'; import 'package:notelytask/cubit/settings_cubit.dart'; -import 'package:notelytask/widgets/github_loader.dart'; import 'package:notelytask/widgets/note_list_layout.dart'; +import 'package:notelytask/widgets/state_loader.dart'; class DeletedListPage extends StatefulWidget { const DeletedListPage({super.key}); @@ -33,7 +33,7 @@ class _DeletedListPageState extends State { backgroundColor: Theme.of(context).colorScheme.primary, bottom: const PreferredSize( preferredSize: Size(double.infinity, 0), - child: GithubLoader(), + child: StateLoader(), ), ), body: PopScope( diff --git a/lib/screens/details_page.dart b/lib/screens/details_page.dart index fe8b5a3..1315f6e 100644 --- a/lib/screens/details_page.dart +++ b/lib/screens/details_page.dart @@ -4,7 +4,7 @@ import 'package:notelytask/cubit/notes_cubit.dart'; import 'package:notelytask/cubit/settings_cubit.dart'; import 'package:notelytask/models/note.dart'; import 'package:notelytask/widgets/details_form.dart'; -import 'package:notelytask/widgets/github_loader.dart'; +import 'package:notelytask/widgets/state_loader.dart'; class DetailsPage extends StatefulWidget { final Note? note; @@ -59,7 +59,7 @@ class _DetailsPageState extends State { ), bottom: const PreferredSize( preferredSize: Size(double.infinity, 0), - child: GithubLoader(), + child: StateLoader(), ), ), body: layout, diff --git a/lib/screens/github_page.dart b/lib/screens/github_page.dart index d9b4d91..cd23619 100644 --- a/lib/screens/github_page.dart +++ b/lib/screens/github_page.dart @@ -8,6 +8,7 @@ import 'package:notelytask/cubit/notes_cubit.dart'; import 'package:notelytask/models/github_state.dart'; import 'package:notelytask/models/notes_state.dart'; import 'package:notelytask/utils.dart'; +import 'package:notelytask/widgets/state_loader.dart'; import 'package:url_launcher/url_launcher.dart'; class GithubPage extends StatefulWidget { @@ -45,9 +46,9 @@ class _GithubPageState extends State { context: context, onPressed: (bool keepLocal) async { await context.read().setRemoteConnection( - repoUrl, - keepLocal, - () => encryptionKeyDialog( + ownerRepo: repoUrl, + keepLocal: keepLocal, + enterEncryptionKeyDialog: () => encryptionKeyDialog( context: context, title: 'Enter Your Encryption Pin', text: @@ -118,6 +119,10 @@ class _GithubPageState extends State { color: Colors.white, ), backgroundColor: Theme.of(context).colorScheme.primary, + bottom: const PreferredSize( + preferredSize: Size(double.infinity, 0), + child: StateLoader(), + ), ), body: BlocBuilder( builder: (context, state) { diff --git a/lib/screens/google_drive_page.dart b/lib/screens/google_drive_page.dart index 5fb49db..4bb84b5 100644 --- a/lib/screens/google_drive_page.dart +++ b/lib/screens/google_drive_page.dart @@ -1,6 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:notelytask/cubit/google_drive_cubit.dart'; +import 'package:notelytask/cubit/notes_cubit.dart'; +import 'package:notelytask/models/google_drive_state.dart'; +import 'package:notelytask/utils.dart'; +import 'package:notelytask/widgets/state_loader.dart'; class GoogleDrivePage extends StatefulWidget { const GoogleDrivePage({super.key}); @@ -10,10 +14,24 @@ class GoogleDrivePage extends StatefulWidget { } class _GoogleDrivePageState extends State { - void _connectGoogleDrive() async { + final fileIdController = TextEditingController(); + String? _fileId; + + void _signInWithGoogle() async { await context.read().getTokens(); } + void _signOut() async { + final result = await context.read().signOut(); + if (!mounted) return; + + if (result) { + context.read().reset(); + } else { + showSnackBar(context, 'Error signing out.'); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -26,26 +44,53 @@ class _GoogleDrivePageState extends State { color: Colors.white, ), backgroundColor: Theme.of(context).colorScheme.primary, - ), - body: Padding( - padding: const EdgeInsets.all(24.0), - child: SizedBox( - width: double.infinity, - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - runAlignment: WrapAlignment.center, - direction: Axis.vertical, - runSpacing: 24.0, - spacing: 12.0, - children: [ - ElevatedButton( - onPressed: _connectGoogleDrive, - child: const Text('Connect to Google Drive'), - ), - ], - ), + bottom: const PreferredSize( + preferredSize: Size(double.infinity, 0), + child: StateLoader(), ), ), + body: BlocBuilder( + builder: (context, state) { + final isLoggedIn = state.isLoggedIn(); + return Padding( + padding: const EdgeInsets.all(24.0), + child: SizedBox( + width: double.infinity, + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + runAlignment: WrapAlignment.center, + direction: Axis.vertical, + runSpacing: 24.0, + spacing: 12.0, + children: [ + if (!isLoggedIn) + ElevatedButton( + onPressed: _signInWithGoogle, + child: const Text('Sign In With Google'), + ), + if (isLoggedIn) + SizedBox( + height: 50.0, + width: MediaQuery.of(context).size.width - 100, + child: TextField( + controller: fileIdController, + onChanged: (value) => setState(() => _fileId = value), + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Existing File ID', + ), + ), + ), + if (isLoggedIn) + ElevatedButton( + onPressed: _signOut, + child: const Text('Sign Out'), + ), + ], + ), + ), + ); + }), ); } } diff --git a/lib/screens/home_page.dart b/lib/screens/home_page.dart index ff5d073..cd09b30 100644 --- a/lib/screens/home_page.dart +++ b/lib/screens/home_page.dart @@ -6,8 +6,8 @@ import 'package:notelytask/cubit/settings_cubit.dart'; import 'package:notelytask/service/native_service.dart'; import 'package:notelytask/service/navigation_service.dart'; import 'package:notelytask/utils.dart'; -import 'package:notelytask/widgets/github_loader.dart'; import 'package:notelytask/widgets/note_list_layout.dart'; +import 'package:notelytask/widgets/state_loader.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -75,7 +75,7 @@ class _HomePageState extends State { ], bottom: const PreferredSize( preferredSize: Size(double.infinity, 0), - child: GithubLoader(), + child: StateLoader(), ), ), body: const NoteListLayout(), diff --git a/lib/widgets/github_loader.dart b/lib/widgets/github_loader.dart deleted file mode 100644 index b856223..0000000 --- a/lib/widgets/github_loader.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:notelytask/cubit/github_cubit.dart'; -import 'package:notelytask/models/github_state.dart'; - -class GithubLoader extends StatelessWidget { - const GithubLoader({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) => state.loading - ? const LinearProgressIndicator( - minHeight: 1, - color: Color(0xffdce3e8), - ) - : Container(), - ); - } -} diff --git a/lib/widgets/state_loader.dart b/lib/widgets/state_loader.dart new file mode 100644 index 0000000..54026e9 --- /dev/null +++ b/lib/widgets/state_loader.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:notelytask/cubit/github_cubit.dart'; +import 'package:notelytask/cubit/google_drive_cubit.dart'; +import 'package:notelytask/models/github_state.dart'; +import 'package:notelytask/models/google_drive_state.dart'; + +class StateLoader extends StatelessWidget { + const StateLoader({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, driveState) => BlocBuilder( + builder: (context, ghState) => (driveState.loading || ghState.loading) + ? const LinearProgressIndicator( + minHeight: 1, + color: Color(0xffdce3e8), + ) + : Container(), + ), + ); + } +} From 2d5169d46c2033abf25da4ee4245920215fa81c1 Mon Sep 17 00:00:00 2001 From: dbilgin Date: Mon, 3 Jun 2024 03:32:13 +0200 Subject: [PATCH 4/5] added google drive notes & files capability --- lib/cubit/github_cubit.dart | 20 +- lib/cubit/google_drive_cubit.dart | 236 +++++++++++++++- lib/cubit/notes_cubit.dart | 106 ++++--- lib/models/file_data.dart | 4 +- lib/models/file_data.g.dart | 4 +- lib/models/google_drive_state.dart | 10 +- lib/models/note.g.dart | 2 +- lib/repository/github_repository.dart | 42 +-- lib/repository/google_drive_repository.dart | 291 +++++++++++++++++++- lib/screens/github_page.dart | 5 +- lib/screens/google_drive_page.dart | 147 +++++++--- lib/utils.dart | 66 ++++- pubspec.lock | 2 +- pubspec.yaml | 1 + 14 files changed, 815 insertions(+), 121 deletions(-) diff --git a/lib/cubit/github_cubit.dart b/lib/cubit/github_cubit.dart index a6b48a6..ea2cdee 100644 --- a/lib/cubit/github_cubit.dart +++ b/lib/cubit/github_cubit.dart @@ -40,7 +40,6 @@ class GithubCubit extends HydratedCubit { Future getRemoteNotes({ required BuildContext context, String? encryptionKey, - String? redirectNoteId, }) async { final accessToken = state.accessToken; final ownerRepo = state.ownerRepo; @@ -98,17 +97,18 @@ class GithubCubit extends HydratedCubit { accessToken, ); + final content = existingFile?.content; + + if (keepLocal || existingFile?.sha == null || content == null) { + return const RemoteConnectionResult(shouldCreateRemote: true); + } + emit( state.copyWith( ownerRepo: ownerRepo, sha: existingFile?.sha, ), ); - final content = existingFile?.content; - - if (keepLocal || existingFile?.sha == null || content == null) { - return const RemoteConnectionResult(shouldCreateRemote: true); - } final isEncryptedString = isEncrypted(content); if (isEncryptedString) { @@ -143,7 +143,7 @@ class GithubCubit extends HydratedCubit { } emit(state.copyWith(loading: true)); - var newFile = await githubRepository.createNewFile( + final newFile = await githubRepository.createNewFile( ownerRepo, accessToken, data, @@ -157,7 +157,7 @@ class GithubCubit extends HydratedCubit { } emit(state.copyWith(loading: false)); - return FileData(name: safeFileName, sha: sha); + return FileData(name: safeFileName, id: sha); } Future deleteFile(FileData fileData) async { @@ -171,7 +171,7 @@ class GithubCubit extends HydratedCubit { bool isDeleted = await githubRepository.deleteFile( ownerRepo, accessToken, - fileData.sha, + fileData.id, fileData.name, ); @@ -197,7 +197,7 @@ class GithubCubit extends HydratedCubit { ? stringifiedContent : encrypt(stringifiedContent, encryptionKey); - var newNote = await githubRepository.createOrUpdateNotesFile( + final newNote = await githubRepository.createOrUpdateNotesFile( ownerRepo, accessToken, finalizedStringContent, diff --git a/lib/cubit/google_drive_cubit.dart b/lib/cubit/google_drive_cubit.dart index 802b33c..fde45db 100644 --- a/lib/cubit/google_drive_cubit.dart +++ b/lib/cubit/google_drive_cubit.dart @@ -1,6 +1,14 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:notelytask/cubit/models/remote_connection_result.dart'; +import 'package:notelytask/models/file_data.dart'; import 'package:notelytask/models/google_drive_state.dart'; import 'package:notelytask/repository/google_drive_repository.dart'; +import 'package:notelytask/repository/models/get_notes_result.dart'; +import 'package:notelytask/utils.dart'; class GoogleDriveCubit extends HydratedCubit { GoogleDriveCubit({ @@ -8,14 +16,146 @@ class GoogleDriveCubit extends HydratedCubit { }) : super(const GoogleDriveState()); final GoogleDriveRepository googleDriveRepository; - Future getTokens() async { + Future getRemoteNotes({ + required BuildContext context, + String? encryptionKey, + }) async { + final fileId = state.fileId; + final accessToken = state.accessToken; + + if (accessToken == null || fileId == null) { + emit(state.copyWith(loading: false)); + return GetNotesResult(); + } + emit(state.copyWith(loading: true)); + + final content = await googleDriveRepository.getExistingNoteFile( + fileId, + accessToken, + getTokens, + ); + + if (content == null || content == '') { + reset(shouldError: true); + return GetNotesResult(); + } + + final isEncryptedString = isEncrypted(content); + + if (isEncryptedString && encryptionKey == null) { + return GetNotesResult(pinNeeded: true); + } + + final decrypted = + isEncryptedString ? decrypt(content, encryptionKey!) : content; + + if (decrypted == null) { + reset(shouldError: true); + return GetNotesResult(); + } + + emit(state.copyWith(loading: false)); + + return GetNotesResult(notesString: decrypted); + } + + Future createOrUpdateRemoteNotes({ + required Map notesJSONMap, + bool shouldResetIfError = true, + String? encryptionKey, + }) async { + final fileId = state.fileId; + final accessToken = state.accessToken; + if (accessToken == null) { + emit(state.copyWith(loading: false)); + return; + } + emit(state.copyWith(loading: true)); + + final stringifiedContent = json.encode(notesJSONMap); + final finalizedStringContent = encryptionKey == null + ? stringifiedContent + : encrypt(stringifiedContent, encryptionKey); + + final newNoteId = await googleDriveRepository.createOrUpdateNotesFile( + fileId, + accessToken, + finalizedStringContent, + getTokens, + ); + + if (newNoteId != null) { + emit(state.copyWith(fileId: newNoteId)); + } else if (shouldResetIfError) { + reset(shouldError: true); + } else { + emit(state.copyWith(error: true, fileId: null)); + } + + emit(state.copyWith(loading: false)); + } + + Future setFileId({ + required String? fileId, + required Future Function() enterEncryptionKeyDialog, + }) async { + final accessToken = state.accessToken; + if (accessToken == null) { + reset(shouldError: true); + return const RemoteConnectionResult(); + } + emit(state.copyWith(loading: true, error: false)); + + if (fileId == null) { + return const RemoteConnectionResult(shouldCreateRemote: true); + } + + final existingContent = await googleDriveRepository.getExistingNoteFile( + fileId, + accessToken, + getTokens, + ); + + if (existingContent == null) { + return const RemoteConnectionResult(shouldCreateRemote: true); + } + + emit( + state.copyWith( + fileId: fileId, + ), + ); + + final isEncryptedString = isEncrypted(existingContent); + if (isEncryptedString) { + final encryptionKey = await enterEncryptionKeyDialog(); + if (encryptionKey == null) { + reset(shouldError: true); + return const RemoteConnectionResult(); + } + + final decrypted = decrypt(existingContent, encryptionKey); + if (decrypted == null) { + reset(shouldError: true); + return const RemoteConnectionResult(); + } + + emit(state.copyWith(loading: false)); + return RemoteConnectionResult(content: decrypted); + } + + emit(state.copyWith(loading: false)); + return RemoteConnectionResult(content: existingContent); + } + + Future getTokens() async { reset(); emit(state.copyWith(loading: true)); final signInData = await googleDriveRepository.signIn(); if (signInData == null) { reset(shouldError: true); - return false; + return null; } final accessToken = signInData.accessToken; @@ -27,8 +167,78 @@ class GoogleDriveCubit extends HydratedCubit { idToken: idToken, ), ); + + emit(state.copyWith(loading: false)); + final verified = await _verify(); + + if (verified) { + return signInData.accessToken; + } else { + return null; + } + } + + Future uploadNewFile( + String safeFileName, + Uint8List data, + ) async { + final accessToken = state.accessToken; + if (accessToken == null) { + return null; + } + emit(state.copyWith(loading: true)); + + final newFile = await googleDriveRepository.createNewFile( + accessToken, + data, + safeFileName, + getTokens, + ); + final fileId = newFile?.fileId; + + if (newFile == null || fileId == null) { + emit(state.copyWith(error: true, loading: false)); + return null; + } + emit(state.copyWith(loading: false)); - return true; + return FileData(name: safeFileName, id: fileId); + } + + Future getFileLocalPath(FileData fileData) async { + final accessToken = state.accessToken; + if (accessToken == null) { + emit(state.copyWith(loading: false)); + return null; + } + + emit(state.copyWith(loading: true)); + + final file = await googleDriveRepository.getFile( + fileData, + accessToken, + getTokens, + ); + + emit(state.copyWith(loading: false)); + return file?.path; + } + + Future deleteFile(FileData fileData) async { + final accessToken = state.accessToken; + if (accessToken == null) { + return false; + } + emit(state.copyWith(loading: true)); + + bool isDeleted = await googleDriveRepository.deleteFile( + accessToken, + fileData, + getTokens, + ); + + emit(state.copyWith(loading: false)); + return isDeleted; } Future signOut() async { @@ -42,6 +252,25 @@ class GoogleDriveCubit extends HydratedCubit { return signedOut; } + Future _verify() async { + final idToken = state.idToken; + if (idToken == null) { + reset(shouldError: true); + return false; + } + try { + final result = await googleDriveRepository.verifyIdToken(idToken); + if (!result) { + reset(shouldError: true); + } + + return result; + } catch (e) { + reset(shouldError: true); + return false; + } + } + void reset({bool shouldError = false}) { emit(const GoogleDriveState()); emit( @@ -50,6 +279,7 @@ class GoogleDriveCubit extends HydratedCubit { error: shouldError, accessToken: null, idToken: null, + fileId: null, ), ); } diff --git a/lib/cubit/notes_cubit.dart b/lib/cubit/notes_cubit.dart index bea7a66..9972f09 100644 --- a/lib/cubit/notes_cubit.dart +++ b/lib/cubit/notes_cubit.dart @@ -32,31 +32,42 @@ class NotesCubit extends HydratedCubit { Future createOrUpdateRemoteNotes({ bool shouldResetIfError = true, }) async { - if (!githubCubit.state.isLoggedIn()) { - return; - } - final jsonMap = state.toJson(); - return await githubCubit.createOrUpdateRemoteNotes( - shouldResetIfError: shouldResetIfError, - encryptionKey: state.encryptionKey, - notesJSONMap: jsonMap, - ); + + if (githubCubit.state.isLoggedIn()) { + return await githubCubit.createOrUpdateRemoteNotes( + shouldResetIfError: shouldResetIfError, + encryptionKey: state.encryptionKey, + notesJSONMap: jsonMap, + ); + } else if (googleDriveCubit.state.accessToken != null) { + return await googleDriveCubit.createOrUpdateRemoteNotes( + shouldResetIfError: shouldResetIfError, + encryptionKey: state.encryptionKey, + notesJSONMap: jsonMap, + ); + } } Future deleteFileAndUpdate(String noteId, FileData fileData) async { - final remoteDeleteResult = await githubCubit.deleteFile(fileData); + final deleteFile = githubCubit.state.isLoggedIn() + ? githubCubit.deleteFile + : googleDriveCubit.deleteFile; + final remoteDeleteResult = await deleteFile(fileData); if (!remoteDeleteResult) return false; _deleteNoteFileData( noteId, fileData.name, ); - createOrUpdateRemoteNotes(); + await createOrUpdateRemoteNotes(); return true; } Future deleteFile(FileData fileData) async { - return await githubCubit.deleteFile(fileData); + final deleteFile = githubCubit.state.isLoggedIn() + ? githubCubit.deleteFile + : googleDriveCubit.deleteFile; + return await deleteFile(fileData); } Future uploadNewFileAndNotes( @@ -68,19 +79,26 @@ class NotesCubit extends HydratedCubit { fileName: fileName, notes: state.notes, ); - final fileData = await githubCubit.uploadNewFile( + + final uploadNewFile = githubCubit.state.isLoggedIn() + ? githubCubit.uploadNewFile + : googleDriveCubit.uploadNewFile; + + final fileData = await uploadNewFile( safeFileName, data, ); - if (fileData == null) return; + if (fileData == null) { + return; + } _addNoteFileData( noteId: noteId, fileName: fileData.name, - fileSha: fileData.sha, + fileId: fileData.id, ); - await createOrUpdateRemoteNotes(); + return await createOrUpdateRemoteNotes(); } bool isLoggedIn() { @@ -88,28 +106,26 @@ class NotesCubit extends HydratedCubit { googleDriveCubit.state.isLoggedIn(); } - Future setRemoteConnection({ + Future setRemoteConnection({ required bool keepLocal, required Future Function() enterEncryptionKeyDialog, String? ownerRepo, + String? fileId, }) async { - // final connectionResult = ownerRepo != null - // ? await githubCubit.setRepoUrl( - // ownerRepo, - // keepLocal, - // enterEncryptionKeyDialog, - // ) - // : await googleDriveCubit.getNotesFile(); - - final connectionResult = await githubCubit.setRepoUrl( - ownerRepo!, - keepLocal, - enterEncryptionKeyDialog, - ); + final connectionResult = ownerRepo != null + ? await githubCubit.setRepoUrl( + ownerRepo, + keepLocal, + enterEncryptionKeyDialog, + ) + : await googleDriveCubit.setFileId( + fileId: fileId, + enterEncryptionKeyDialog: enterEncryptionKeyDialog, + ); if (connectionResult.shouldCreateRemote) { await createOrUpdateRemoteNotes(shouldResetIfError: false); - return; + return true; } final content = connectionResult.content; @@ -118,7 +134,9 @@ class NotesCubit extends HydratedCubit { final finalContent = json.decode(content); final notes = fromJson(finalContent); emit(notes); + return true; } + return false; } void reset({ @@ -141,15 +159,20 @@ class NotesCubit extends HydratedCubit { required BuildContext context, String? redirectNoteId, }) async { - if (!githubCubit.state.isLoggedIn()) { + if (!githubCubit.state.isLoggedIn() && + !googleDriveCubit.state.isLoggedIn()) { return; } - final result = await githubCubit.getRemoteNotes( + final getRemoteNotes = githubCubit.state.isLoggedIn() + ? githubCubit.getRemoteNotes + : googleDriveCubit.getRemoteNotes; + + final result = await getRemoteNotes( context: context, encryptionKey: state.encryptionKey, - redirectNoteId: redirectNoteId, ); + final notesString = result.notesString; if (result.pinNeeded && context.mounted) { @@ -199,8 +222,13 @@ class NotesCubit extends HydratedCubit { } } - Future getFileLocalPath(String fileName) async { - return await githubCubit.getFileLocalPath(fileName); + Future getFileLocalPath(FileData fileData) async { + if (githubCubit.state.isLoggedIn()) { + return await githubCubit.getFileLocalPath(fileData.name); + } else if (googleDriveCubit.state.isLoggedIn()) { + return await googleDriveCubit.getFileLocalPath(fileData); + } + return null; } void setEncryptionKey(String? key) { @@ -273,19 +301,19 @@ class NotesCubit extends HydratedCubit { void _addNoteFileData({ required String noteId, required String fileName, - required String fileSha, + required String fileId, }) { final noteIndex = state.notes.indexWhere((element) => element.id == noteId); List updatedNotes = List.from(state.notes); if (noteIndex == -1) { final newNote = Note.generateNew() - .copyWith(fileDataList: [FileData(name: fileName, sha: fileSha)]); + .copyWith(fileDataList: [FileData(name: fileName, id: fileId)]); updatedNotes.add(newNote); } else { List updatedFileDataList = List.from(state.notes[noteIndex].fileDataList) - ..add(FileData(name: fileName, sha: fileSha)); + ..add(FileData(name: fileName, id: fileId)); updatedNotes[noteIndex] = state.notes[noteIndex].copyWith(fileDataList: updatedFileDataList); } diff --git a/lib/models/file_data.dart b/lib/models/file_data.dart index 413b5ed..374cf22 100644 --- a/lib/models/file_data.dart +++ b/lib/models/file_data.dart @@ -5,11 +5,11 @@ part 'file_data.g.dart'; @JsonSerializable(explicitToJson: true) class FileData { final String name; - final String sha; + final String id; FileData({ required this.name, - required this.sha, + required this.id, }); factory FileData.fromJson(Map json) => diff --git a/lib/models/file_data.g.dart b/lib/models/file_data.g.dart index 1230cdd..cbd2c01 100644 --- a/lib/models/file_data.g.dart +++ b/lib/models/file_data.g.dart @@ -8,10 +8,10 @@ part of 'file_data.dart'; FileData _$FileDataFromJson(Map json) => FileData( name: json['name'] as String, - sha: json['sha'] as String, + id: json['id'] as String, ); Map _$FileDataToJson(FileData instance) => { 'name': instance.name, - 'sha': instance.sha, + 'id': instance.id, }; diff --git a/lib/models/google_drive_state.dart b/lib/models/google_drive_state.dart index af748e0..89363dc 100644 --- a/lib/models/google_drive_state.dart +++ b/lib/models/google_drive_state.dart @@ -6,18 +6,21 @@ class GoogleDriveState extends Equatable { final String? accessToken; final String? idToken; + final String? fileId; const GoogleDriveState({ this.loading = false, this.error = false, this.accessToken, this.idToken, + this.fileId, }); bool isLoggedIn() { final idToken = this.idToken; final accessToken = this.accessToken; - return idToken != null && accessToken != null; + final fileId = this.fileId; + return idToken != null && accessToken != null && fileId != null; } factory GoogleDriveState.fromJson(Map json) { @@ -26,6 +29,7 @@ class GoogleDriveState extends Equatable { error: json['error'] as bool? ?? false, accessToken: json['accessToken'] as String?, idToken: json['idToken'] as String?, + fileId: json['fileId'] as String?, ); } @@ -34,6 +38,7 @@ class GoogleDriveState extends Equatable { 'error': error, 'accessToken': accessToken, 'idToken': idToken, + 'fileId': fileId, }; GoogleDriveState copyWith({ @@ -41,12 +46,14 @@ class GoogleDriveState extends Equatable { bool? error, String? accessToken, String? idToken, + String? fileId, }) { return GoogleDriveState( loading: loading ?? this.loading, error: error ?? this.error, accessToken: accessToken ?? this.accessToken, idToken: idToken ?? this.idToken, + fileId: fileId ?? this.fileId, ); } @@ -56,5 +63,6 @@ class GoogleDriveState extends Equatable { error, accessToken, idToken, + fileId, ]; } diff --git a/lib/models/note.g.dart b/lib/models/note.g.dart index c18dc46..35b3dfa 100644 --- a/lib/models/note.g.dart +++ b/lib/models/note.g.dart @@ -23,6 +23,6 @@ Map _$NoteToJson(Note instance) => { 'title': instance.title, 'text': instance.text, 'date': instance.date.toIso8601String(), - 'fileDataList': instance.fileDataList, + 'fileDataList': instance.fileDataList.map((e) => e.toJson()).toList(), 'isDeleted': instance.isDeleted, }; diff --git a/lib/repository/github_repository.dart b/lib/repository/github_repository.dart index 779ee6c..a62e4aa 100644 --- a/lib/repository/github_repository.dart +++ b/lib/repository/github_repository.dart @@ -32,17 +32,17 @@ class GithubRepository { 'message': '$fileName uploaded', 'content': encodedContent, }; - var response = await put( + final response = await put( url, headers: {'Authorization': 'bearer $accessToken'}, body: jsonEncode(body), ); if (response.statusCode >= 200 && response.statusCode < 300) { - var jsonResponse = json.decode(response.body) as Map; + final jsonResponse = json.decode(response.body) as Map; - var content = jsonResponse['content']; - var sha = content['sha']; + final content = jsonResponse['content']; + final sha = content['sha']; return GithubFile( sha: sha, @@ -70,7 +70,7 @@ class GithubRepository { 'message': '$fileName deleted', 'sha': sha, }; - var response = await delete( + final response = await delete( url, headers: {'Authorization': 'bearer $accessToken'}, body: jsonEncode(body), @@ -110,17 +110,17 @@ class GithubRepository { 'message': 'Notes added', 'content': encodedContent, }; - var response = await put( + final response = await put( url, headers: {'Authorization': 'bearer $accessToken'}, body: jsonEncode(body), ); if (response.statusCode >= 200 && response.statusCode < 300) { - var jsonResponse = json.decode(response.body) as Map; + final jsonResponse = json.decode(response.body) as Map; - var content = jsonResponse['content']; - var sha = content['sha']; + final content = jsonResponse['content']; + final sha = content['sha']; return GithubFile( sha: sha, @@ -143,7 +143,7 @@ class GithubRepository { 'api.github.com', '/repos/$ownerRepo/contents/$fileName', ); - var response = await get( + final response = await get( url, headers: { 'Authorization': 'bearer $accessToken', @@ -182,13 +182,13 @@ class GithubRepository { 'api.github.com', '/repos/$ownerRepo/contents/notes.json', ); - var response = await get( + final response = await get( url, headers: {'Authorization': 'bearer $accessToken'}, ); if (response.statusCode >= 200 && response.statusCode < 300) { - var jsonResponse = json.decode(response.body); + final jsonResponse = json.decode(response.body); final sha = jsonResponse['sha']; @@ -219,15 +219,15 @@ class GithubRepository { }, ); - var response = await post(url, headers: {'Accept': 'application/json'}); + final response = await post(url, headers: {'Accept': 'application/json'}); if (response.statusCode >= 200 && response.statusCode < 300) { - var jsonResponse = jsonDecode(response.body) as Map; + final jsonResponse = jsonDecode(response.body) as Map; - var deviceCode = jsonResponse['device_code']; - var userCode = jsonResponse['user_code']; - var verificationUri = jsonResponse['verification_uri']; - var expiresIn = jsonResponse['expires_in']; + final deviceCode = jsonResponse['device_code']; + final userCode = jsonResponse['user_code']; + final verificationUri = jsonResponse['verification_uri']; + final expiresIn = jsonResponse['expires_in']; return GithubState( deviceCode: deviceCode, @@ -266,12 +266,12 @@ class GithubRepository { ); } try { - var response = await post(url, headers: {'Accept': 'application/json'}); + final response = await post(url, headers: {'Accept': 'application/json'}); if (response.statusCode >= 200 && response.statusCode < 300) { - var jsonResponse = jsonDecode(response.body) as Map; + final jsonResponse = jsonDecode(response.body) as Map; - var accessToken = jsonResponse['access_token']; + final accessToken = jsonResponse['access_token']; return accessToken; } else { return null; diff --git a/lib/repository/google_drive_repository.dart b/lib/repository/google_drive_repository.dart index 15606c4..46fad05 100644 --- a/lib/repository/google_drive_repository.dart +++ b/lib/repository/google_drive_repository.dart @@ -1,12 +1,27 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:flutter/foundation.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:http/http.dart'; +import 'package:http_parser/http_parser.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:notelytask/cubit/models/google_sign_in_result.dart'; +import 'package:notelytask/models/file_data.dart'; +import 'package:path_provider/path_provider.dart'; + +class GoogleFile { + GoogleFile({this.fileId, this.content}); + final String? fileId; + final String? content; +} class GoogleDriveRepository { final List scopes = [ 'https://www.googleapis.com/auth/drive.appdata', - 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/drive.file', // write + 'https://www.googleapis.com/auth/drive.readonly', // read ]; Future signIn() async { try { @@ -50,4 +65,278 @@ class GoogleDriveRepository { return false; } } + + Future verifyIdToken( + String idToken, + ) async { + try { + final url = Uri.https( + 'api.notelytask.com', + '/google_verify', + { + 'id_token': idToken, + }, + ); + final response = await post(url, headers: {'Accept': 'application/json'}); + + if ((response.statusCode >= 200 && response.statusCode < 300) || + response.statusCode == 404) { + return true; + } else { + return false; + } + } catch (e) { + return false; + } + } + + Future getFile( + FileData fileData, + String accessToken, + Future Function() getTokens, + ) async { + try { + final url = Uri.https( + 'www.googleapis.com', + '/drive/v3/files/${fileData.id}', + {'alt': 'media'}, + ); + final response = await get( + url, + headers: {'Authorization': 'Bearer $accessToken'}, + ); + + if (response.statusCode >= 200 && response.statusCode < 300) { + Directory dir = kIsWeb + ? HydratedStorage.webStorageDirectory + : await getTemporaryDirectory(); + + final path = '${dir.path}/${fileData.name}'; + + final file = File(path); + await file.writeAsBytes( + response.bodyBytes, + mode: FileMode.write, + ); + + return file; + } else if (response.statusCode == 401) { + final newAccessToken = await getTokens(); + if (newAccessToken == null) { + return null; + } + return getFile(fileData, newAccessToken, getTokens); + } + return null; + } catch (e) { + return null; + } + } + + Future deleteFile( + String accessToken, + FileData fileData, + Future Function() getTokens, + ) async { + try { + final url = Uri.https( + 'www.googleapis.com', + '/drive/v3/files/${fileData.id}', + ); + final response = await delete( + url, + headers: {'Authorization': 'Bearer $accessToken'}, + ); + + if ((response.statusCode >= 200 && response.statusCode < 300) || + response.statusCode == 404) { + return true; + } else if (response.statusCode == 401) { + final newAccessToken = await getTokens(); + if (newAccessToken == null) { + return false; + } + return deleteFile(newAccessToken, fileData, getTokens); + } else { + return false; + } + } catch (e) { + return false; + } + } + + Future getExistingNoteFile( + String fileId, + String accessToken, + Future Function() getTokens, + ) async { + try { + final url = Uri.https( + 'www.googleapis.com', + '/drive/v3/files/$fileId', + { + 'alt': 'media', + }, + ); + final response = await get( + url, + headers: {'Authorization': 'Bearer $accessToken'}, + ); + + if (response.statusCode >= 200 && response.statusCode < 300) { + final cleanedJson = response.body.replaceAll('\n', '').trim(); + return cleanedJson; + } else if (response.statusCode == 401) { + final newAccessToken = await getTokens(); + if (newAccessToken == null) { + return null; + } + return getExistingNoteFile(fileId, newAccessToken, getTokens); + } else { + return null; + } + } catch (e) { + return null; + } + } + + Future createNewFile( + String accessToken, + Uint8List content, + String fileName, + Future Function() getTokens, + ) async { + try { + final url = Uri.https( + 'www.googleapis.com', + '/upload/drive/v3/files', + { + 'uploadType': 'multipart', + }, + ); + + final params = { + 'name': fileName, + 'title': fileName, + 'parents': [], + // 'mimeType': 'text/plain', + }; + final request = MultipartRequest('POST', url) + ..headers['Authorization'] = 'Bearer $accessToken' + ..headers['Content-Type'] = 'multipart/related' + ..files.add(MultipartFile.fromString( + 'metadata', + json.encode(params), + contentType: MediaType('application', 'json', {'charset': 'UTF-8'}), + filename: fileName, + )) + ..files.add(MultipartFile.fromBytes( + 'file', + content, + // contentType: MediaType('text', 'plain'), + filename: fileName, + )); + + final response = await request.send().then((result) async { + return Response.fromStream(result); + }); + + if (response.statusCode >= 200 && response.statusCode < 300) { + final jsonResponse = json.decode(response.body) as Map; + + return GoogleFile( + fileId: jsonResponse['id'], + ); + } else if (response.statusCode == 401) { + final newAccessToken = await getTokens(); + if (newAccessToken == null) { + return null; + } + return createNewFile( + newAccessToken, + content, + fileName, + getTokens, + ); + } else { + return null; + } + } catch (e) { + return null; + } + } + + Future createOrUpdateNotesFile( + String? fileId, + String accessToken, + String stringifiedContent, + Future Function() getTokens, + ) async { + try { + final url = fileId != null + ? Uri.https( + 'www.googleapis.com', + '/upload/drive/v3/files/$fileId', + { + 'uploadType': 'multipart', + }, + ) + : Uri.https( + 'www.googleapis.com', + '/upload/drive/v3/files', + { + 'uploadType': 'multipart', + }, + ); + + final params = { + 'name': 'notes.json', + 'title': 'notes.json', + 'parents': [], + 'mimeType': 'text/plain', + }; + final requestMethod = fileId != null ? 'PATCH' : 'POST'; + + final request = MultipartRequest(requestMethod, url) + ..headers['Authorization'] = 'Bearer $accessToken' + ..headers['Content-Type'] = 'multipart/related' + ..files.add(MultipartFile.fromString( + 'metadata', + json.encode(params), + contentType: MediaType('application', 'json', {'charset': 'UTF-8'}), + filename: 'notes.json', + )) + ..files.add(MultipartFile.fromString( + 'file', + stringifiedContent, + contentType: MediaType('text', 'plain'), + filename: 'notes.json', + )); + + final response = await request.send().then((result) async { + return Response.fromStream(result); + }); + + if (response.statusCode >= 200 && response.statusCode < 300) { + final jsonResponse = json.decode(response.body) as Map; + final id = jsonResponse['id']; + return id; + } else if (response.statusCode == 401) { + final newAccessToken = await getTokens(); + if (newAccessToken == null) { + return null; + } + return createOrUpdateNotesFile( + fileId, + newAccessToken, + stringifiedContent, + getTokens, + ); + } else { + return null; + } + } catch (e) { + return null; + } + } } diff --git a/lib/screens/github_page.dart b/lib/screens/github_page.dart index cd23619..ee7682b 100644 --- a/lib/screens/github_page.dart +++ b/lib/screens/github_page.dart @@ -45,7 +45,7 @@ class _GithubPageState extends State { saveToRepoAlert( context: context, onPressed: (bool keepLocal) async { - await context.read().setRemoteConnection( + final result = await context.read().setRemoteConnection( ownerRepo: repoUrl, keepLocal: keepLocal, enterEncryptionKeyDialog: () => encryptionKeyDialog( @@ -56,6 +56,9 @@ class _GithubPageState extends State { isPinRequired: true, ), ); + if (!result && mounted) { + showSnackBar(context, 'An error occurred.'); + } }, ); } diff --git a/lib/screens/google_drive_page.dart b/lib/screens/google_drive_page.dart index 4bb84b5..9c5cd0e 100644 --- a/lib/screens/google_drive_page.dart +++ b/lib/screens/google_drive_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:notelytask/cubit/google_drive_cubit.dart'; import 'package:notelytask/cubit/notes_cubit.dart'; import 'package:notelytask/models/google_drive_state.dart'; +import 'package:notelytask/models/notes_state.dart'; import 'package:notelytask/utils.dart'; import 'package:notelytask/widgets/state_loader.dart'; @@ -14,22 +15,74 @@ class GoogleDrivePage extends StatefulWidget { } class _GoogleDrivePageState extends State { - final fileIdController = TextEditingController(); - String? _fileId; - void _signInWithGoogle() async { - await context.read().getTokens(); + final result = await context.read().getTokens(); + if (!mounted) { + return; + } + + if (result == null) { + showSnackBar(context, 'Error signing in.'); + return; + } + + _setFileId(); + } + + Future _setFileId() async { + final fileIdResult = await googleFileIdDialog(context); + if (!mounted) { + return; + } + + final result = await context.read().setRemoteConnection( + fileId: fileIdResult, + keepLocal: fileIdResult == null, + enterEncryptionKeyDialog: () => encryptionKeyDialog( + context: context, + title: 'Enter Your Encryption Pin', + text: + 'This will be used to decrypt your notes.\nLeave blank if you do not have a key.', + isPinRequired: true, + ), + ); + + if (!result && mounted) { + showSnackBar(context, 'An error occurred.'); + } } void _signOut() async { final result = await context.read().signOut(); if (!mounted) return; - if (result) { - context.read().reset(); - } else { - showSnackBar(context, 'Error signing out.'); + context.read().reset(shouldError: result); + } + + Future _onSubmitEncryption(String key) async { + context.read().setEncryptionKey(key); + await context.read().createOrUpdateRemoteNotes(); + + if (!mounted) return; + await context.read().getAndUpdateLocalNotes(context: context); + if (!mounted) return; + showSnackBar(context, 'Encryption successful.'); + } + + Future _onSubmitDecryption(String key) async { + final existingKey = context.read().state.encryptionKey; + if (key != existingKey) { + showSnackBar(context, 'Wrong pin, decryption failed.'); + return; } + + context.read().setEncryptionKey(null); + await context.read().createOrUpdateRemoteNotes(); + + if (!mounted) return; + await context.read().getAndUpdateLocalNotes(context: context); + if (!mounted) return; + showSnackBar(context, 'Decryption successful.'); } @override @@ -56,38 +109,58 @@ class _GoogleDrivePageState extends State { padding: const EdgeInsets.all(24.0), child: SizedBox( width: double.infinity, - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - runAlignment: WrapAlignment.center, - direction: Axis.vertical, - runSpacing: 24.0, - spacing: 12.0, - children: [ - if (!isLoggedIn) - ElevatedButton( + child: isLoggedIn + ? Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + runAlignment: WrapAlignment.center, + direction: Axis.vertical, + runSpacing: 24.0, + spacing: 12.0, + children: [ + Text('File ID: ${state.fileId}'), + ElevatedButton( + onPressed: _signOut, + child: const Text('Sign Out'), + ), + BlocBuilder(builder: ( + notesContext, + notesState, + ) { + return Wrap( + children: [ + if (notesState.encryptionKey == null) + ElevatedButton( + onPressed: () => encryptionKeyDialog( + context: context, + isPinRequired: false, + title: 'Enter Your Encryption Pin', + text: + 'Do not lose this!\nThis will encrypt your notes.', + onSubmit: _onSubmitEncryption, + ), + child: const Text('Encrypt Notes'), + ), + if (notesState.encryptionKey != null) + ElevatedButton( + onPressed: () => encryptionKeyDialog( + context: context, + isPinRequired: false, + title: 'Enter Your Encryption Pin', + text: + 'Decryption will fail if wrong key is entered.', + onSubmit: _onSubmitDecryption, + ), + child: const Text('Decrypt Notes'), + ), + ], + ); + }), + ], + ) + : ElevatedButton( onPressed: _signInWithGoogle, child: const Text('Sign In With Google'), ), - if (isLoggedIn) - SizedBox( - height: 50.0, - width: MediaQuery.of(context).size.width - 100, - child: TextField( - controller: fileIdController, - onChanged: (value) => setState(() => _fileId = value), - decoration: const InputDecoration( - border: OutlineInputBorder(), - labelText: 'Existing File ID', - ), - ), - ), - if (isLoggedIn) - ElevatedButton( - onPressed: _signOut, - child: const Text('Sign Out'), - ), - ], - ), ), ); }), diff --git a/lib/utils.dart b/lib/utils.dart index 2219f74..1b1e549 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -204,8 +204,7 @@ Future openFile(BuildContext context, FileData file) async { Future _openFileWithData(BuildContext context, FileData file) async { try { - final filePath = - await context.read().getFileLocalPath(file.name); + final filePath = await context.read().getFileLocalPath(file); if (filePath != null) { var res = await OpenFilex.open(filePath); @@ -416,3 +415,66 @@ String nonExistentFileName({ ); } } + +Future googleFileIdDialog( + BuildContext context, +) async { + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + final fileIdController = TextEditingController(); + String? fileId; + + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + title: const Text('Add/Create File ID'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.only(bottom: 16.0), + child: Text( + 'Leave empty to create a new file.', + textAlign: TextAlign.center, + ), + ), + SizedBox( + height: 50.0, + width: MediaQuery.of(context).size.width - 100, + child: TextField( + controller: fileIdController, + onChanged: (value) => setState(() => fileId = value), + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Existing File ID ', + ), + ), + ), + ], + ), + actions: [ + TextButton( + style: TextButton.styleFrom( + textStyle: Theme.of(context).textTheme.labelLarge, + ), + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(null); + }, + ), + TextButton( + style: TextButton.styleFrom( + textStyle: Theme.of(context).textTheme.labelLarge, + ), + onPressed: () => Navigator.of(context).pop(fileId), + child: const Text('Set'), + ), + ], + ); + }, + ); + }, + ); +} diff --git a/pubspec.lock b/pubspec.lock index 2443eef..d9c49e4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -657,7 +657,7 @@ packages: source: hosted version: "3.2.1" http_parser: - dependency: transitive + dependency: "direct main" description: name: http_parser sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" diff --git a/pubspec.yaml b/pubspec.yaml index 97e71c5..c6b8aa7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: flutter_markdown: ^0.6.22 flutter_widget_from_html: ^0.14.11 google_sign_in: ^6.2.1 + http_parser: ^4.0.2 dev_dependencies: flutter_test: From 8c9c9b11a7387671a9bd2afcefaa64e31f0d5e39 Mon Sep 17 00:00:00 2001 From: dbilgin Date: Mon, 3 Jun 2024 03:48:45 +0200 Subject: [PATCH 5/5] Check state in AppBar --- lib/screens/home_page.dart | 42 +++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/lib/screens/home_page.dart b/lib/screens/home_page.dart index cd09b30..c5f4a74 100644 --- a/lib/screens/home_page.dart +++ b/lib/screens/home_page.dart @@ -1,8 +1,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:notelytask/cubit/github_cubit.dart'; +import 'package:notelytask/cubit/google_drive_cubit.dart'; import 'package:notelytask/cubit/notes_cubit.dart'; import 'package:notelytask/cubit/settings_cubit.dart'; +import 'package:notelytask/models/github_state.dart'; +import 'package:notelytask/models/google_drive_state.dart'; import 'package:notelytask/service/native_service.dart'; import 'package:notelytask/service/navigation_service.dart'; import 'package:notelytask/utils.dart'; @@ -62,16 +66,34 @@ class _HomePageState extends State { onPressed: _navigateToDeletedList, color: Colors.white, ), - IconButton( - icon: Image.asset('assets/github.png'), - tooltip: 'Github Integration', - onPressed: _navigateToGithubLogin, - ), - IconButton( - icon: Image.asset('assets/google_drive.png'), - tooltip: 'Google Drive Integration', - onPressed: _navigateToGDriveLogin, - ), + BlocBuilder(builder: ( + googleDriveContext, + googleDriveState, + ) { + if (!googleDriveState.isLoggedIn()) { + return IconButton( + icon: Image.asset('assets/github.png'), + tooltip: 'Github Integration', + onPressed: _navigateToGithubLogin, + ); + } else { + return Container(); + } + }), + BlocBuilder(builder: ( + githubContext, + githubState, + ) { + if (!githubState.isLoggedIn()) { + return IconButton( + icon: Image.asset('assets/google_drive.png'), + tooltip: 'Google Drive Integration', + onPressed: _navigateToGDriveLogin, + ); + } else { + return Container(); + } + }), ], bottom: const PreferredSize( preferredSize: Size(double.infinity, 0),