From 5212d7bf706090cd4532bfc5a487c6ae8d193448 Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Sat, 20 Feb 2021 14:10:25 -0500 Subject: [PATCH] Documentation, Basic Login Workflow --- .gitignore | 2 + README.md | 34 +- internal/api/auth.go | 57 +- web_native/README.md | 22 +- web_native/graphql/mediaItems.graphql | 18 + web_native/ios/Podfile.lock | 18 +- .../contents.xcworkspacedata | 2 +- web_native/lib/api/api_provider.dart | 134 ++--- web_native/lib/api/browser_cookie_client.dart | 30 - .../cookie_client/browser_cookie_client.dart | 36 ++ .../cookie_client.dart} | 2 +- .../api/cookie_client/io_cookie_client.dart | 43 ++ .../lib/api/imagini_api_repository.dart | 27 +- web_native/lib/api/io_cookie_client.dart | 28 - web_native/lib/blocs/login_bloc.dart | 40 ++ web_native/lib/blocs/splash_bloc.dart | 36 -- web_native/lib/core/app_component.dart | 1 - web_native/lib/core/app_routes.dart | 14 +- web_native/lib/core/imagini_application.dart | 14 +- .../storage_client/base_storage_client.dart | 4 + .../browser_storage_client.dart | 19 + .../storage_client/mobile_storage_client.dart | 19 + .../core/storage_client/storage_client.dart | 4 + .../lib/graphql/imagini_graphql.graphql.dart | 522 ++++++++++++++++++ .../graphql/imagini_graphql.graphql.g.dart | 334 +++++++++++ web_native/lib/screens/login_screen.dart | 138 ++++- web_native/lib/screens/splash_screen.dart | 70 --- 27 files changed, 1327 insertions(+), 341 deletions(-) create mode 100644 web_native/graphql/mediaItems.graphql delete mode 100644 web_native/lib/api/browser_cookie_client.dart create mode 100644 web_native/lib/api/cookie_client/browser_cookie_client.dart rename web_native/lib/api/{cookie_client_stub.dart => cookie_client/cookie_client.dart} (60%) create mode 100644 web_native/lib/api/cookie_client/io_cookie_client.dart delete mode 100644 web_native/lib/api/io_cookie_client.dart create mode 100644 web_native/lib/blocs/login_bloc.dart delete mode 100644 web_native/lib/blocs/splash_bloc.dart create mode 100644 web_native/lib/core/storage_client/base_storage_client.dart create mode 100644 web_native/lib/core/storage_client/browser_storage_client.dart create mode 100644 web_native/lib/core/storage_client/mobile_storage_client.dart create mode 100644 web_native/lib/core/storage_client/storage_client.dart delete mode 100644 web_native/lib/screens/splash_screen.dart diff --git a/.gitignore b/.gitignore index 1575361..a970ee2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +imagini.db +media/ notes web/node_modules/ web/dist/ diff --git a/README.md b/README.md index fcd8f09..9ca208b 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,32 @@ -#uImagini +# Imagini +A self hosted photo library with user management & authentication. Cross platform Client supporting Android, iOS, and Web. -## Running Server - # cd ./cmd/ - CONFIG_PATH=$(pwd) DATA_PATH=$(pwd) go run main.go serve +## Server +### Running -## Generate GraphQL Models - # cd ./cmd/ + CONFIG_PATH=$(pwd) DATA_PATH=$(pwd) go run cmd/main.go serve + +## Building + + # Generate GraphQL Models go run github.com/99designs/gqlgen generate - go run main.go generate + go run cmd/main.go generate -## Generate GraphQL Documentation - # From app root + # Generate GraphQL Documentation graphdoc -e http://localhost:8484/query -o ./docs/schema -## Server Build +## Client +See `web_native` subfolder. +### Running -## Flutter Build + # Chrome + flutter run -d chrome + + # Simulator + open -a Simulator flutter run -## Generate GraphQL Flutter Models +### Building + + # Generate GraphQL Flutter Models flutter pub run build_runner build diff --git a/internal/api/auth.go b/internal/api/auth.go index 297d6c1..e357195 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -41,35 +41,46 @@ func (api *API) refreshTokens(refreshToken jwt.Token) (string, string, error) { } // Update Access Token - accessTokenCookie, err := api.Auth.CreateJWTAccessToken(user, device) + accessToken, err := api.Auth.CreateJWTAccessToken(user, device) if err != nil { return "", "", err } - return accessTokenCookie, "", err + return accessToken, "", err } func (api *API) validateTokens(w *http.ResponseWriter, r *http.Request) (jwt.Token, error) { - // TODO: Check from X-Imagini-AccessToken - // TODO: Check from X-Imagini-RefreshToken - - // Validate Access Token - accessCookie, _ := r.Cookie("AccessToken") - if accessCookie != nil { - accessToken, err := api.Auth.ValidateJWTAccessToken(accessCookie.Value) + accessTokenHeader := r.Header.Get("X-Imagini-AccessToken") + if accessTokenHeader != "" { + accessToken, err := api.Auth.ValidateJWTAccessToken(accessTokenHeader) if err == nil { return accessToken, nil } } - // Validate Refresh Cookie Exists - refreshCookie, _ := r.Cookie("RefreshToken") - if refreshCookie == nil { + refreshTokenHeader := r.Header.Get("X-Imagini-RefreshToken") + if refreshTokenHeader == "" { return nil, errors.New("Tokens Invalid") } + // Validate Access Token + // accessCookie, _ := r.Cookie("AccessToken") + // if accessCookie != nil { + // accessToken, err := api.Auth.ValidateJWTAccessToken(accessCookie.Value) + // if err == nil { + // return accessToken, nil + // } + // } + + // Validate Refresh Cookie Exists + // refreshCookie, _ := r.Cookie("RefreshToken") + // if refreshCookie == nil { + // return nil, errors.New("Tokens Invalid") + // } + // Validate Refresh Token - refreshToken, err := api.Auth.ValidateJWTRefreshToken(refreshCookie.Value) + // refreshToken, err := api.Auth.ValidateJWTRefreshToken(refreshCookie.Value) + refreshToken, err := api.Auth.ValidateJWTRefreshToken(refreshTokenHeader) if err != nil { return nil, errors.New("Tokens Invalid") } @@ -81,21 +92,21 @@ func (api *API) validateTokens(w *http.ResponseWriter, r *http.Request) (jwt.Tok } // TODO: Actually Refresh Refresh Token - newRefreshToken = refreshCookie.Value + // newRefreshToken = refreshCookie.Value + newRefreshToken = refreshTokenHeader // Set appropriate cookies (TODO: Only for web!) // Update Access & Refresh Cookies - http.SetCookie(*w, &http.Cookie{ - Name: "AccessToken", - Value: newAccessToken, - }) - http.SetCookie(*w, &http.Cookie{ - Name: "RefreshToken", - Value: newRefreshToken, - }) + // http.SetCookie(*w, &http.Cookie{ + // Name: "AccessToken", + // Value: newAccessToken, + // }) + // http.SetCookie(*w, &http.Cookie{ + // Name: "RefreshToken", + // Value: newRefreshToken, + // }) - // Only for iOS & Android (TODO: Remove for web! Only cause affected by CORS during development) (*w).Header().Set("X-Imagini-AccessToken", newAccessToken) (*w).Header().Set("X-Imagini-RefreshToken", newRefreshToken) diff --git a/web_native/README.md b/web_native/README.md index 0b980d1..6d867ab 100644 --- a/web_native/README.md +++ b/web_native/README.md @@ -1,16 +1,16 @@ -# imagini +# Imagini Client +A cross platform (iOS, Android, & Web) client used with the Imagini server. -A new Flutter project. +## Running -## Getting Started + # Chrome + flutter run -d chrome -This project is a starting point for a Flutter application. + # Simulator + open -a Simulator + flutter run -A few resources to get you started if this is your first Flutter project: +## Building -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. + # Generate GraphQL Flutter Models + flutter pub run build_runner build diff --git a/web_native/graphql/mediaItems.graphql b/web_native/graphql/mediaItems.graphql new file mode 100644 index 0000000..582746a --- /dev/null +++ b/web_native/graphql/mediaItems.graphql @@ -0,0 +1,18 @@ +query mediaItems($order: Order, $page: Page, $filter: MediaItemFilter) { + mediaItems(filter: $filter, page: $page, order: $order) { + data { + id + fileName + latitude + longitude + isVideo + origName + createdAt + } + page { + size + page + total + } + } +} diff --git a/web_native/ios/Podfile.lock b/web_native/ios/Podfile.lock index 51d516e..9e71cbf 100644 --- a/web_native/ios/Podfile.lock +++ b/web_native/ios/Podfile.lock @@ -1,4 +1,7 @@ PODS: + - connectivity (0.0.1): + - Flutter + - Reachability - DKImagePickerController/Core (4.3.2): - DKImagePickerController/ImageDataManager - DKImagePickerController/Resource @@ -38,6 +41,9 @@ PODS: - Flutter - integration_test (0.0.1): - Flutter + - path_provider (0.0.1): + - Flutter + - Reachability (3.2) - SDWebImage (5.10.2): - SDWebImage/Core (= 5.10.2) - SDWebImage/Core (5.10.2) @@ -48,10 +54,12 @@ PODS: - Flutter DEPENDENCIES: + - connectivity (from `.symlinks/plugins/connectivity/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) + - path_provider (from `.symlinks/plugins/path_provider/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) - url_launcher (from `.symlinks/plugins/url_launcher/ios`) @@ -59,10 +67,13 @@ SPEC REPOS: trunk: - DKImagePickerController - DKPhotoGallery + - Reachability - SDWebImage - SwiftyGif EXTERNAL SOURCES: + connectivity: + :path: ".symlinks/plugins/connectivity/ios" file_picker: :path: ".symlinks/plugins/file_picker/ios" Flutter: @@ -71,18 +82,23 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_secure_storage/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" + path_provider: + :path: ".symlinks/plugins/path_provider/ios" shared_preferences: :path: ".symlinks/plugins/shared_preferences/ios" url_launcher: :path: ".symlinks/plugins/url_launcher/ios" SPEC CHECKSUMS: + connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467 DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec - integration_test: 5ed24a436eb7ec17b6a13046e9bf7ca4a404e59e + integration_test: 6eb66a19f7104200dcfdd62bc0077e1b09686e4f + path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c + Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 SDWebImage: b969dcfc02c40a5da71eac0b03b8f1a0c794a86f shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d SwiftyGif: e466e86c660d343357ab944a819a101c4127cb40 diff --git a/web_native/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/web_native/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a1..919434a 100644 --- a/web_native/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/web_native/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/web_native/lib/api/api_provider.dart b/web_native/lib/api/api_provider.dart index 7b48f0d..b1647cf 100644 --- a/web_native/lib/api/api_provider.dart +++ b/web_native/lib/api/api_provider.dart @@ -1,61 +1,51 @@ import 'dart:async'; -// ignore: uri_does_not_exist -import 'cookie_client_stub.dart' - // ignore: uri_does_not_exist - if (dart.library.html) 'browser_cookie_client.dart' - // ignore: uri_does_not_exist - if (dart.library.io) 'io_cookie_client.dart'; +import 'package:imagini/api/cookie_client/cookie_client.dart' + if (dart.library.html) 'package:imagini/api/cookie_client/browser_cookie_client.dart' + if (dart.library.io) 'package:imagini/api/cookie_client/io_cookie_client.dart'; -import 'package:meta/meta.dart'; +import 'package:imagini/core/storage_client/base_storage_client.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:imagini/graphql/imagini_graphql.dart'; class APIProvider{ - String _server, _accessToken, _refreshToken; - - GraphQLClient _client; - HttpLink httpLink; - // CookieLink cookieLink; static const String _GRAPHQL_ENDPOINT = "/query"; - APIProvider({ - @required String server, - String accessToken, - String refreshToken - }) { - _server = server; - _accessToken = accessToken; - _refreshToken = refreshToken; + BaseStorageClient _storage; + GraphQLClient _client; + HttpLink httpLink; + + APIProvider(BaseStorageClient storage) { + _storage = storage; + init(); + } + + Future init() async { + String _server = await _storage.get("server"); + httpLink = HttpLink(_server + _GRAPHQL_ENDPOINT, - httpClient: getCookieClient(), + httpClient: getCookieClient(_storage), ); - // cookieLink = CookieLink(_updateAccessToken, _updateRefreshToken); _client = GraphQLClient( cache: GraphQLCache(), link: httpLink, ); } - // void _updateAccessToken(_accessToken) { - // print("Updating Access Token: $_accessToken"); - // this._accessToken = _accessToken; - // } - - // void _updateRefreshToken(_refreshToken) { - // print("Updating Refresh Token: $_accessToken"); - // this._refreshToken = _refreshToken; - // } - - Future login([ + Future login( String username, String password, - ]) async { + String server, + ) async { assert( - (username != null && password != null) + (username != null && password != null && server != null) ); + // Initialize Connection + await _storage.set("server", server); + await init(); + QueryResult response = await _client.query( QueryOptions( document: LoginQuery().document, @@ -65,79 +55,27 @@ class APIProvider{ }, ) ); - - final loginResponse = Login$Query.fromJson(response.data); - return loginResponse.login; + return response; } - Future me() async { + Future me() async { QueryResult response = await _client.query( QueryOptions( document: MeQuery().document, ) ); - - final meResponse = Me$Query.fromJson(response.data); - return meResponse.me; + return response; } - Future mediaItems([ - String startDate, - String endDate, - String albumID, - List tagID, - String type, // TODO: Make enum - int page, - ]) async { - // Query: - // /api/v1/MediaItems - // Derive Params: - // startDate: - // &createdAt=>2020-10-10T10:10:10 - // endDate: - // &createdAt=<2020-10-10T10:10:10 - // albumID: - // &albumID=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d - // tagID: - // &tagID=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d,9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d - // type: - // &type=Photos - // &type=Videos - // page: - // &page=4 + Future mediaItems() async { + QueryResult response = await _client.query( + QueryOptions( + document: MediaItemsQuery().document, + ) + ); - // Returns: - // { - // - // } - - return null; - } - - Future tags([ - int page - ]) async { - // Query: - // /api/v1/Tags - // Derive Params: - // page: - // &page=4 - return null; - } - - Future albums([ - int page - ]) async { - // Query: - // /api/v1/Albums - // Derive Params: - // page: - // &page=4 - return null; - } - - Future me() async { - return null; + print(response); + return response; } void dispose() {} diff --git a/web_native/lib/api/browser_cookie_client.dart b/web_native/lib/api/browser_cookie_client.dart deleted file mode 100644 index 8e8de01..0000000 --- a/web_native/lib/api/browser_cookie_client.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:http/browser_client.dart'; -import "package:http/http.dart"; - -BaseClient getCookieClient() => ClientWithCookies(); - -class ClientWithCookies extends BrowserClient { - String _accessToken = "asdasdasd"; - String _refreshToken; - - @override - Future send(BaseRequest request) async { - request.headers.addAll({ - 'X-Imagini-AccessToken': _accessToken, - 'X-Imagini-RefreshToken': _refreshToken, - }); - - return super.send(request).then((response) { - if (response.headers.containsKey("x-imagini-accesstoken")) { - this._accessToken = response.headers["x-imagini-accesstoken"]; - } - if (response.headers.containsKey("x-imagini-refreshtoken")) { - this._refreshToken = response.headers["x-imagini-refreshtoken"]; - } - - print("Access Token: $_accessToken"); - print("Refresh Token: $_refreshToken"); - return response; - }); - } -} diff --git a/web_native/lib/api/cookie_client/browser_cookie_client.dart b/web_native/lib/api/cookie_client/browser_cookie_client.dart new file mode 100644 index 0000000..53fba47 --- /dev/null +++ b/web_native/lib/api/cookie_client/browser_cookie_client.dart @@ -0,0 +1,36 @@ +import 'package:http/browser_client.dart'; +import "package:http/http.dart"; +import "package:imagini/core/storage_client/base_storage_client.dart"; + +BaseClient getCookieClient(storage) => ClientWithCookies(storage); + +class ClientWithCookies extends BrowserClient { + BaseStorageClient _storage; + + ClientWithCookies(BaseStorageClient storage) { + _storage = storage; + } + + @override + Future send(BaseRequest request) async { + String _accessToken = await _storage.get("accessToken"); + String _refreshToken = await _storage.get("refreshToken"); + + request.headers.addAll({ + 'X-Imagini-AccessToken': _accessToken, + 'X-Imagini-RefreshToken': _refreshToken, + }); + + return super.send(request).then((response) async { + // We've been told to update our access token + if (response.headers.containsKey("x-imagini-accesstoken")) { + await _storage.set("accessToken", response.headers["x-imagini-accesstoken"]); + } + // We've been told to update our refresh token + if (response.headers.containsKey("x-imagini-refreshtoken")) { + await _storage.set("refreshToken", response.headers["x-imagini-refreshtoken"]); + } + return response; + }); + } +} diff --git a/web_native/lib/api/cookie_client_stub.dart b/web_native/lib/api/cookie_client/cookie_client.dart similarity index 60% rename from web_native/lib/api/cookie_client_stub.dart rename to web_native/lib/api/cookie_client/cookie_client.dart index 216be4a..9fd9007 100644 --- a/web_native/lib/api/cookie_client_stub.dart +++ b/web_native/lib/api/cookie_client/cookie_client.dart @@ -1,4 +1,4 @@ import 'package:http/http.dart'; -BaseClient getCookieClient() => throw UnsupportedError( +BaseClient getCookieClient(storage) => throw UnsupportedError( 'Cannot create a client without dart:html or dart:io.'); diff --git a/web_native/lib/api/cookie_client/io_cookie_client.dart b/web_native/lib/api/cookie_client/io_cookie_client.dart new file mode 100644 index 0000000..26a4508 --- /dev/null +++ b/web_native/lib/api/cookie_client/io_cookie_client.dart @@ -0,0 +1,43 @@ +import 'package:http/io_client.dart'; +import "package:http/http.dart"; +import "package:imagini/core/storage_client/base_storage_client.dart"; + +BaseClient getCookieClient(storage) => IOClientWithCookies(storage); + +class IOClientWithCookies extends IOClient { + BaseStorageClient _storage; + + IOClientWithCookies(BaseStorageClient storage) { + _storage = storage; + } + + @override + Future send(BaseRequest request) async { + String _accessToken = await _storage.get("accessToken"); + String _refreshToken = await _storage.get("refreshToken"); + + request.headers.addAll({ + 'X-Imagini-AccessToken': _accessToken, + 'X-Imagini-RefreshToken': _refreshToken, + }); + + return super.send(request).then((response) async { + // We've been told to update our access token + if (response.headers.containsKey("x-imagini-accesstoken")) { + await _storage.set("accessToken", response.headers["x-imagini-accesstoken"]); + } + // We've been told to update our refresh token + if (response.headers.containsKey("x-imagini-refreshtoken")) { + await _storage.set("refreshToken", response.headers["x-imagini-refreshtoken"]); + } + + _accessToken = await _storage.get("accessToken"); + _refreshToken = await _storage.get("refreshToken"); + + print("Access Token: $_accessToken"); + print("Refresh Token: $_refreshToken"); + + return response; + }); + } +} diff --git a/web_native/lib/api/imagini_api_repository.dart b/web_native/lib/api/imagini_api_repository.dart index 32bb3e8..657bd53 100644 --- a/web_native/lib/api/imagini_api_repository.dart +++ b/web_native/lib/api/imagini_api_repository.dart @@ -1,12 +1,35 @@ +import 'dart:async'; import 'package:imagini/api/api_provider.dart'; import 'package:imagini/graphql/imagini_graphql.dart'; +import 'package:graphql_flutter/graphql_flutter.dart'; class ImaginiAPIRepository { APIProvider _apiProvider; ImaginiAPIRepository(this._apiProvider); - Stream login(String user, password) { - return Stream.fromFuture(_apiProvider.login(user, password)); + Stream login(String user, password, server) { + return Stream.fromFuture(_apiProvider.login(user, password, server).then((QueryResult resp) { + if (resp.exception != null) + return false; + + final loginResponse = Login$Query.fromJson(resp.data); + if (loginResponse.login.result == AuthResult.failure) + return false; + + return true; + })); + } + + Stream me() { + return Stream.fromFuture(_apiProvider.me()); + } + + Stream isAuthenticated() { + return Stream.fromFuture(_apiProvider.me().then((QueryResult resp) { + if (resp.exception != null) + return false; + return true; + })); } } diff --git a/web_native/lib/api/io_cookie_client.dart b/web_native/lib/api/io_cookie_client.dart deleted file mode 100644 index f9e954f..0000000 --- a/web_native/lib/api/io_cookie_client.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:http/io_client.dart'; -import "package:http/http.dart"; - -BaseClient getCookieClient() => IOClientWithCookies(); - -class IOClientWithCookies extends IOClient { - String _accessToken; - String _refreshToken; - - @override - Future send(BaseRequest request) async { - // String cookie = await getCookie(); - // String getCookieString(String _) => cookie; - // request.headers.update('cookie', getCookieString); - return super.send(request).then((response) { - if (response.headers.containsKey("x-imagini-accesstoken")) { - this._accessToken = response.headers["x-imagini-accesstoken"]; - } - if (response.headers.containsKey("x-imagini-refreshtoken")) { - this._refreshToken = response.headers["x-imagini-refreshtoken"]; - } - - print("Access Token: $_accessToken"); - print("Refresh Token: $_refreshToken"); - return response; - }); - } -} diff --git a/web_native/lib/blocs/login_bloc.dart b/web_native/lib/blocs/login_bloc.dart new file mode 100644 index 0000000..2b7f029 --- /dev/null +++ b/web_native/lib/blocs/login_bloc.dart @@ -0,0 +1,40 @@ +import 'dart:async'; + +import 'package:imagini/core/imagini_application.dart'; +import 'package:imagini/api/imagini_api_repository.dart'; + +class LoginBloc{ + + final ImaginiApplication _application; + ImaginiAPIRepository _imaginiAPI; + + final _loginController = StreamController.broadcast(); + Stream get loginResult => _loginController.stream; + + final _authenticatedController = StreamController.broadcast(); + Stream get authenticatedResult => _authenticatedController.stream; + + LoginBloc(this._application){ + _init(); + } + + void _init(){ + _imaginiAPI = _application.imaginiAPI; + checkAuthentication(); + + // attemptLogin("admin", "admin", "http://localhost:8484"); + } + + void dispose(){ + _loginController.close(); + _authenticatedController.close(); + } + + checkAuthentication(){ + _authenticatedController.addStream(_imaginiAPI.isAuthenticated()); + } + + attemptLogin(String username, password, server){ + _loginController.addStream(_imaginiAPI.login(username, password, server)); + } +} diff --git a/web_native/lib/blocs/splash_bloc.dart b/web_native/lib/blocs/splash_bloc.dart deleted file mode 100644 index 782866b..0000000 --- a/web_native/lib/blocs/splash_bloc.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'dart:async'; - -import 'package:imagini/core/imagini_application.dart'; -import 'package:imagini/api/imagini_api_repository.dart'; -import 'package:imagini/graphql/imagini_graphql.dart'; - -class SplashBloc{ - - final ImaginiApplication _application; - - final _loginController = StreamController(); - Stream get loginResult => _loginController.stream; - - SplashBloc(this._application){ - _init(); - } - - void _init(){ - // Do Initial Load - initializeLogin(); - } - - void dispose(){ - _loginController.close(); - } - - initializeLogin(){ - ImaginiAPIRepository imaginiAPI = _application.imaginiAPI; - - // TODO: This should actually attempt to load the existing Tokens, not login - _loginController.addStream(imaginiAPI.login("admin", "admin")); - - // imaginiAPI.login("admin", "admin1").listen((LoginResponse lr) { - // }); - } -} diff --git a/web_native/lib/core/app_component.dart b/web_native/lib/core/app_component.dart index 7f785f5..c049ad0 100644 --- a/web_native/lib/core/app_component.dart +++ b/web_native/lib/core/app_component.dart @@ -24,7 +24,6 @@ class AppComponentState extends State { @override void dispose() async { - // Log.info('dispose'); super.dispose(); await _application.onTerminate(); } diff --git a/web_native/lib/core/app_routes.dart b/web_native/lib/core/app_routes.dart index 3127066..a40be2c 100644 --- a/web_native/lib/core/app_routes.dart +++ b/web_native/lib/core/app_routes.dart @@ -3,13 +3,13 @@ import 'package:flutter/material.dart'; import 'package:imagini/screens/home_screen.dart'; import 'package:imagini/screens/login_screen.dart'; -import 'package:imagini/screens/splash_screen.dart'; +// import 'package:imagini/screens/splash_screen.dart'; -var splashHandler = new Handler( - handlerFunc: (BuildContext context, Map> params) { - return SplashScreen(); - } -); +// var splashHandler = new Handler( +// handlerFunc: (BuildContext context, Map> params) { +// return SplashScreen(); +// } +// ); var loginHandler = new Handler( handlerFunc: (BuildContext context, Map> params) { @@ -42,7 +42,7 @@ class AppRoutes { return; } ); - router.define(SplashScreen.PATH, handler: splashHandler); + // router.define(SplashScreen.PATH, handler: splashHandler); router.define(LoginScreen.PATH, handler: loginHandler); router.define(HomeScreen.PATH, handler: homeHandler); // router.define(AppDetailPage.PATH, handler: appDetailRouteHandler); diff --git a/web_native/lib/core/imagini_application.dart b/web_native/lib/core/imagini_application.dart index 1834ba1..e26675d 100644 --- a/web_native/lib/core/imagini_application.dart +++ b/web_native/lib/core/imagini_application.dart @@ -3,13 +3,20 @@ import 'package:fluro/fluro.dart'; import 'package:imagini/core/app_routes.dart'; import 'package:imagini/api/api_provider.dart'; import 'package:imagini/api/imagini_api_repository.dart'; +import 'package:imagini/core/storage_client/base_storage_client.dart'; + +import 'package:imagini/core/storage_client/storage_client.dart' + if (dart.library.html) 'package:imagini/core/storage_client/browser_storage_client.dart' + if (dart.library.io) 'package:imagini/core/storage_client/mobile_storage_client.dart'; class ImaginiApplication { FluroRouter router; ImaginiAPIRepository imaginiAPI; + BaseStorageClient storageClient; Future onCreate() async { _initRouter(); + _initStorageClient(); _initAPIRepository(); } @@ -20,8 +27,13 @@ class ImaginiApplication { AppRoutes.configureRoutes(router); } + _initStorageClient() { + storageClient = getStorageClient(); + } + _initAPIRepository() { - APIProvider apiProvider = new APIProvider(server: "http://localhost:8484"); + // TODO: Get from config + APIProvider apiProvider = new APIProvider(storageClient); imaginiAPI = ImaginiAPIRepository(apiProvider); } } diff --git a/web_native/lib/core/storage_client/base_storage_client.dart b/web_native/lib/core/storage_client/base_storage_client.dart new file mode 100644 index 0000000..e99fc55 --- /dev/null +++ b/web_native/lib/core/storage_client/base_storage_client.dart @@ -0,0 +1,4 @@ +abstract class BaseStorageClient { + Future get(String key); + Future set(String key, String value); +} diff --git a/web_native/lib/core/storage_client/browser_storage_client.dart b/web_native/lib/core/storage_client/browser_storage_client.dart new file mode 100644 index 0000000..433cb76 --- /dev/null +++ b/web_native/lib/core/storage_client/browser_storage_client.dart @@ -0,0 +1,19 @@ +import 'dart:html'; + +import './base_storage_client.dart'; + +BaseStorageClient getStorageClient() => BrowserStorageClient(); + +class BrowserStorageClient extends BaseStorageClient { + + @override + Future get(String key) async { + var requestedValue = window.localStorage.containsKey(key) ? window.localStorage[key] : ""; + return requestedValue; + } + + @override + Future set(String key, String value) async { + window.localStorage[key] = value; + } +} diff --git a/web_native/lib/core/storage_client/mobile_storage_client.dart b/web_native/lib/core/storage_client/mobile_storage_client.dart new file mode 100644 index 0000000..7f54a13 --- /dev/null +++ b/web_native/lib/core/storage_client/mobile_storage_client.dart @@ -0,0 +1,19 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +import './base_storage_client.dart'; + +BaseStorageClient getStorageClient() => MobileStorageClient(); + +class MobileStorageClient extends BaseStorageClient { + final storage = new FlutterSecureStorage(); + + @override + Future get(String key) async { + return storage.read(key: key); + } + + @override + Future set(String key, String value) async { + return storage.write(key: key, value: value); + } +} diff --git a/web_native/lib/core/storage_client/storage_client.dart b/web_native/lib/core/storage_client/storage_client.dart new file mode 100644 index 0000000..b11c7e1 --- /dev/null +++ b/web_native/lib/core/storage_client/storage_client.dart @@ -0,0 +1,4 @@ +import './base_storage_client.dart'; + +BaseStorageClient getStorageClient() => throw UnsupportedError( + 'Cannot create a storage client.'); diff --git a/web_native/lib/graphql/imagini_graphql.graphql.dart b/web_native/lib/graphql/imagini_graphql.graphql.dart index 9d3f397..130f48f 100644 --- a/web_native/lib/graphql/imagini_graphql.graphql.dart +++ b/web_native/lib/graphql/imagini_graphql.graphql.dart @@ -170,6 +170,362 @@ class Me$Query with EquatableMixin { Map toJson() => _$Me$QueryToJson(this); } +@JsonSerializable(explicitToJson: true) +class MediaItems$Query$MediaItemResponse$MediaItem with EquatableMixin { + MediaItems$Query$MediaItemResponse$MediaItem(); + + factory MediaItems$Query$MediaItemResponse$MediaItem.fromJson( + Map json) => + _$MediaItems$Query$MediaItemResponse$MediaItemFromJson(json); + + String id; + + String fileName; + + double latitude; + + double longitude; + + bool isVideo; + + String origName; + + DateTime createdAt; + + @override + List get props => + [id, fileName, latitude, longitude, isVideo, origName, createdAt]; + Map toJson() => + _$MediaItems$Query$MediaItemResponse$MediaItemToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class MediaItems$Query$MediaItemResponse$PageResponse with EquatableMixin { + MediaItems$Query$MediaItemResponse$PageResponse(); + + factory MediaItems$Query$MediaItemResponse$PageResponse.fromJson( + Map json) => + _$MediaItems$Query$MediaItemResponse$PageResponseFromJson(json); + + int size; + + int page; + + int total; + + @override + List get props => [size, page, total]; + Map toJson() => + _$MediaItems$Query$MediaItemResponse$PageResponseToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class MediaItems$Query$MediaItemResponse with EquatableMixin { + MediaItems$Query$MediaItemResponse(); + + factory MediaItems$Query$MediaItemResponse.fromJson( + Map json) => + _$MediaItems$Query$MediaItemResponseFromJson(json); + + List data; + + MediaItems$Query$MediaItemResponse$PageResponse page; + + @override + List get props => [data, page]; + Map toJson() => + _$MediaItems$Query$MediaItemResponseToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class MediaItems$Query with EquatableMixin { + MediaItems$Query(); + + factory MediaItems$Query.fromJson(Map json) => + _$MediaItems$QueryFromJson(json); + + MediaItems$Query$MediaItemResponse mediaItems; + + @override + List get props => [mediaItems]; + Map toJson() => _$MediaItems$QueryToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class TimeFilter with EquatableMixin { + TimeFilter( + {this.equalTo, + this.notEqualTo, + this.lessThan, + this.lessThanOrEqualTo, + this.greaterThan, + this.greaterThanOrEqualTo}); + + factory TimeFilter.fromJson(Map json) => + _$TimeFilterFromJson(json); + + DateTime equalTo; + + DateTime notEqualTo; + + DateTime lessThan; + + DateTime lessThanOrEqualTo; + + DateTime greaterThan; + + DateTime greaterThanOrEqualTo; + + @override + List get props => [ + equalTo, + notEqualTo, + lessThan, + lessThanOrEqualTo, + greaterThan, + greaterThanOrEqualTo + ]; + Map toJson() => _$TimeFilterToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class FloatFilter with EquatableMixin { + FloatFilter( + {this.equalTo, + this.notEqualTo, + this.lessThan, + this.lessThanOrEqualTo, + this.greaterThan, + this.greaterThanOrEqualTo}); + + factory FloatFilter.fromJson(Map json) => + _$FloatFilterFromJson(json); + + double equalTo; + + double notEqualTo; + + double lessThan; + + double lessThanOrEqualTo; + + double greaterThan; + + double greaterThanOrEqualTo; + + @override + List get props => [ + equalTo, + notEqualTo, + lessThan, + lessThanOrEqualTo, + greaterThan, + greaterThanOrEqualTo + ]; + Map toJson() => _$FloatFilterToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class BooleanFilter with EquatableMixin { + BooleanFilter({this.equalTo, this.notEqualTo}); + + factory BooleanFilter.fromJson(Map json) => + _$BooleanFilterFromJson(json); + + bool equalTo; + + bool notEqualTo; + + @override + List get props => [equalTo, notEqualTo]; + Map toJson() => _$BooleanFilterToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class IDFilter with EquatableMixin { + IDFilter({this.equalTo, this.notEqualTo}); + + factory IDFilter.fromJson(Map json) => + _$IDFilterFromJson(json); + + String equalTo; + + String notEqualTo; + + @override + List get props => [equalTo, notEqualTo]; + Map toJson() => _$IDFilterToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class StringFilter with EquatableMixin { + StringFilter( + {this.equalTo, + this.notEqualTo, + this.startsWith, + this.notStartsWith, + this.endsWith, + this.notEndsWith, + this.contains, + this.notContains}); + + factory StringFilter.fromJson(Map json) => + _$StringFilterFromJson(json); + + String equalTo; + + String notEqualTo; + + String startsWith; + + String notStartsWith; + + String endsWith; + + String notEndsWith; + + String contains; + + String notContains; + + @override + List get props => [ + equalTo, + notEqualTo, + startsWith, + notStartsWith, + endsWith, + notEndsWith, + contains, + notContains + ]; + Map toJson() => _$StringFilterToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class MediaItemFilter with EquatableMixin { + MediaItemFilter( + {this.id, + this.createdAt, + this.updatedAt, + this.exifDate, + this.latitude, + this.longitude, + this.isVideo, + this.origName, + this.tags, + this.albums}); + + factory MediaItemFilter.fromJson(Map json) => + _$MediaItemFilterFromJson(json); + + IDFilter id; + + TimeFilter createdAt; + + TimeFilter updatedAt; + + TimeFilter exifDate; + + FloatFilter latitude; + + FloatFilter longitude; + + BooleanFilter isVideo; + + StringFilter origName; + + TagFilter tags; + + AlbumFilter albums; + + @override + List get props => [ + id, + createdAt, + updatedAt, + exifDate, + latitude, + longitude, + isVideo, + origName, + tags, + albums + ]; + Map toJson() => _$MediaItemFilterToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class TagFilter with EquatableMixin { + TagFilter({this.id, this.createdAt, this.updatedAt, this.name}); + + factory TagFilter.fromJson(Map json) => + _$TagFilterFromJson(json); + + IDFilter id; + + TimeFilter createdAt; + + TimeFilter updatedAt; + + StringFilter name; + + @override + List get props => [id, createdAt, updatedAt, name]; + Map toJson() => _$TagFilterToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class AlbumFilter with EquatableMixin { + AlbumFilter({this.id, this.createdAt, this.updatedAt, this.name}); + + factory AlbumFilter.fromJson(Map json) => + _$AlbumFilterFromJson(json); + + IDFilter id; + + TimeFilter createdAt; + + TimeFilter updatedAt; + + StringFilter name; + + @override + List get props => [id, createdAt, updatedAt, name]; + Map toJson() => _$AlbumFilterToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class Page with EquatableMixin { + Page({this.size, this.page}); + + factory Page.fromJson(Map json) => _$PageFromJson(json); + + int size; + + int page; + + @override + List get props => [size, page]; + Map toJson() => _$PageToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class Order with EquatableMixin { + Order({this.by, this.direction}); + + factory Order.fromJson(Map json) => _$OrderFromJson(json); + + String by; + + @JsonKey(unknownEnumValue: OrderDirection.artemisUnknown) + OrderDirection direction; + + @override + List get props => [by, direction]; + Map toJson() => _$OrderToJson(this); +} + enum AuthResult { @JsonValue('Success') success, @@ -194,6 +550,14 @@ enum AuthType { @JsonValue('ARTEMIS_UNKNOWN') artemisUnknown, } +enum OrderDirection { + @JsonValue('ASC') + asc, + @JsonValue('DESC') + desc, + @JsonValue('ARTEMIS_UNKNOWN') + artemisUnknown, +} @JsonSerializable(explicitToJson: true) class LoginArguments extends JsonSerializable with EquatableMixin { @@ -508,3 +872,161 @@ class MeQuery extends GraphQLQuery { @override Me$Query parse(Map json) => Me$Query.fromJson(json); } + +@JsonSerializable(explicitToJson: true) +class MediaItemsArguments extends JsonSerializable with EquatableMixin { + MediaItemsArguments({this.order, this.page, this.filter}); + + @override + factory MediaItemsArguments.fromJson(Map json) => + _$MediaItemsArgumentsFromJson(json); + + final Order order; + + final Page page; + + final MediaItemFilter filter; + + @override + List get props => [order, page, filter]; + @override + Map toJson() => _$MediaItemsArgumentsToJson(this); +} + +class MediaItemsQuery + extends GraphQLQuery { + MediaItemsQuery({this.variables}); + + @override + final DocumentNode document = DocumentNode(definitions: [ + OperationDefinitionNode( + type: OperationType.query, + name: NameNode(value: 'mediaItems'), + variableDefinitions: [ + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'order')), + type: NamedTypeNode( + name: NameNode(value: 'Order'), isNonNull: false), + defaultValue: DefaultValueNode(value: null), + directives: []), + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'page')), + type: NamedTypeNode( + name: NameNode(value: 'Page'), isNonNull: false), + defaultValue: DefaultValueNode(value: null), + directives: []), + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'filter')), + type: NamedTypeNode( + name: NameNode(value: 'MediaItemFilter'), isNonNull: false), + defaultValue: DefaultValueNode(value: null), + directives: []) + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'mediaItems'), + alias: null, + arguments: [ + ArgumentNode( + name: NameNode(value: 'filter'), + value: VariableNode(name: NameNode(value: 'filter'))), + ArgumentNode( + name: NameNode(value: 'page'), + value: VariableNode(name: NameNode(value: 'page'))), + ArgumentNode( + name: NameNode(value: 'order'), + value: VariableNode(name: NameNode(value: 'order'))) + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'data'), + alias: null, + arguments: [], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'id'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'fileName'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'latitude'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'longitude'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'isVideo'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'origName'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'createdAt'), + alias: null, + arguments: [], + directives: [], + selectionSet: null) + ])), + FieldNode( + name: NameNode(value: 'page'), + alias: null, + arguments: [], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'size'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'page'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'total'), + alias: null, + arguments: [], + directives: [], + selectionSet: null) + ])) + ])) + ])) + ]); + + @override + final String operationName = 'mediaItems'; + + @override + final MediaItemsArguments variables; + + @override + List get props => [document, operationName, variables]; + @override + MediaItems$Query parse(Map json) => + MediaItems$Query.fromJson(json); +} diff --git a/web_native/lib/graphql/imagini_graphql.graphql.g.dart b/web_native/lib/graphql/imagini_graphql.graphql.g.dart index b45581c..c2d3ce3 100644 --- a/web_native/lib/graphql/imagini_graphql.graphql.g.dart +++ b/web_native/lib/graphql/imagini_graphql.graphql.g.dart @@ -192,6 +192,318 @@ Map _$Me$QueryToJson(Me$Query instance) => { 'me': instance.me?.toJson(), }; +MediaItems$Query$MediaItemResponse$MediaItem + _$MediaItems$Query$MediaItemResponse$MediaItemFromJson( + Map json) { + return MediaItems$Query$MediaItemResponse$MediaItem() + ..id = json['id'] as String + ..fileName = json['fileName'] as String + ..latitude = (json['latitude'] as num)?.toDouble() + ..longitude = (json['longitude'] as num)?.toDouble() + ..isVideo = json['isVideo'] as bool + ..origName = json['origName'] as String + ..createdAt = json['createdAt'] == null + ? null + : DateTime.parse(json['createdAt'] as String); +} + +Map _$MediaItems$Query$MediaItemResponse$MediaItemToJson( + MediaItems$Query$MediaItemResponse$MediaItem instance) => + { + 'id': instance.id, + 'fileName': instance.fileName, + 'latitude': instance.latitude, + 'longitude': instance.longitude, + 'isVideo': instance.isVideo, + 'origName': instance.origName, + 'createdAt': instance.createdAt?.toIso8601String(), + }; + +MediaItems$Query$MediaItemResponse$PageResponse + _$MediaItems$Query$MediaItemResponse$PageResponseFromJson( + Map json) { + return MediaItems$Query$MediaItemResponse$PageResponse() + ..size = json['size'] as int + ..page = json['page'] as int + ..total = json['total'] as int; +} + +Map _$MediaItems$Query$MediaItemResponse$PageResponseToJson( + MediaItems$Query$MediaItemResponse$PageResponse instance) => + { + 'size': instance.size, + 'page': instance.page, + 'total': instance.total, + }; + +MediaItems$Query$MediaItemResponse _$MediaItems$Query$MediaItemResponseFromJson( + Map json) { + return MediaItems$Query$MediaItemResponse() + ..data = (json['data'] as List) + ?.map((e) => e == null + ? null + : MediaItems$Query$MediaItemResponse$MediaItem.fromJson( + e as Map)) + ?.toList() + ..page = json['page'] == null + ? null + : MediaItems$Query$MediaItemResponse$PageResponse.fromJson( + json['page'] as Map); +} + +Map _$MediaItems$Query$MediaItemResponseToJson( + MediaItems$Query$MediaItemResponse instance) => + { + 'data': instance.data?.map((e) => e?.toJson())?.toList(), + 'page': instance.page?.toJson(), + }; + +MediaItems$Query _$MediaItems$QueryFromJson(Map json) { + return MediaItems$Query() + ..mediaItems = json['mediaItems'] == null + ? null + : MediaItems$Query$MediaItemResponse.fromJson( + json['mediaItems'] as Map); +} + +Map _$MediaItems$QueryToJson(MediaItems$Query instance) => + { + 'mediaItems': instance.mediaItems?.toJson(), + }; + +TimeFilter _$TimeFilterFromJson(Map json) { + return TimeFilter( + equalTo: json['equalTo'] == null + ? null + : DateTime.parse(json['equalTo'] as String), + notEqualTo: json['notEqualTo'] == null + ? null + : DateTime.parse(json['notEqualTo'] as String), + lessThan: json['lessThan'] == null + ? null + : DateTime.parse(json['lessThan'] as String), + lessThanOrEqualTo: json['lessThanOrEqualTo'] == null + ? null + : DateTime.parse(json['lessThanOrEqualTo'] as String), + greaterThan: json['greaterThan'] == null + ? null + : DateTime.parse(json['greaterThan'] as String), + greaterThanOrEqualTo: json['greaterThanOrEqualTo'] == null + ? null + : DateTime.parse(json['greaterThanOrEqualTo'] as String), + ); +} + +Map _$TimeFilterToJson(TimeFilter instance) => + { + 'equalTo': instance.equalTo?.toIso8601String(), + 'notEqualTo': instance.notEqualTo?.toIso8601String(), + 'lessThan': instance.lessThan?.toIso8601String(), + 'lessThanOrEqualTo': instance.lessThanOrEqualTo?.toIso8601String(), + 'greaterThan': instance.greaterThan?.toIso8601String(), + 'greaterThanOrEqualTo': instance.greaterThanOrEqualTo?.toIso8601String(), + }; + +FloatFilter _$FloatFilterFromJson(Map json) { + return FloatFilter( + equalTo: (json['equalTo'] as num)?.toDouble(), + notEqualTo: (json['notEqualTo'] as num)?.toDouble(), + lessThan: (json['lessThan'] as num)?.toDouble(), + lessThanOrEqualTo: (json['lessThanOrEqualTo'] as num)?.toDouble(), + greaterThan: (json['greaterThan'] as num)?.toDouble(), + greaterThanOrEqualTo: (json['greaterThanOrEqualTo'] as num)?.toDouble(), + ); +} + +Map _$FloatFilterToJson(FloatFilter instance) => + { + 'equalTo': instance.equalTo, + 'notEqualTo': instance.notEqualTo, + 'lessThan': instance.lessThan, + 'lessThanOrEqualTo': instance.lessThanOrEqualTo, + 'greaterThan': instance.greaterThan, + 'greaterThanOrEqualTo': instance.greaterThanOrEqualTo, + }; + +BooleanFilter _$BooleanFilterFromJson(Map json) { + return BooleanFilter( + equalTo: json['equalTo'] as bool, + notEqualTo: json['notEqualTo'] as bool, + ); +} + +Map _$BooleanFilterToJson(BooleanFilter instance) => + { + 'equalTo': instance.equalTo, + 'notEqualTo': instance.notEqualTo, + }; + +IDFilter _$IDFilterFromJson(Map json) { + return IDFilter( + equalTo: json['equalTo'] as String, + notEqualTo: json['notEqualTo'] as String, + ); +} + +Map _$IDFilterToJson(IDFilter instance) => { + 'equalTo': instance.equalTo, + 'notEqualTo': instance.notEqualTo, + }; + +StringFilter _$StringFilterFromJson(Map json) { + return StringFilter( + equalTo: json['equalTo'] as String, + notEqualTo: json['notEqualTo'] as String, + startsWith: json['startsWith'] as String, + notStartsWith: json['notStartsWith'] as String, + endsWith: json['endsWith'] as String, + notEndsWith: json['notEndsWith'] as String, + contains: json['contains'] as String, + notContains: json['notContains'] as String, + ); +} + +Map _$StringFilterToJson(StringFilter instance) => + { + 'equalTo': instance.equalTo, + 'notEqualTo': instance.notEqualTo, + 'startsWith': instance.startsWith, + 'notStartsWith': instance.notStartsWith, + 'endsWith': instance.endsWith, + 'notEndsWith': instance.notEndsWith, + 'contains': instance.contains, + 'notContains': instance.notContains, + }; + +MediaItemFilter _$MediaItemFilterFromJson(Map json) { + return MediaItemFilter( + id: json['id'] == null + ? null + : IDFilter.fromJson(json['id'] as Map), + createdAt: json['createdAt'] == null + ? null + : TimeFilter.fromJson(json['createdAt'] as Map), + updatedAt: json['updatedAt'] == null + ? null + : TimeFilter.fromJson(json['updatedAt'] as Map), + exifDate: json['exifDate'] == null + ? null + : TimeFilter.fromJson(json['exifDate'] as Map), + latitude: json['latitude'] == null + ? null + : FloatFilter.fromJson(json['latitude'] as Map), + longitude: json['longitude'] == null + ? null + : FloatFilter.fromJson(json['longitude'] as Map), + isVideo: json['isVideo'] == null + ? null + : BooleanFilter.fromJson(json['isVideo'] as Map), + origName: json['origName'] == null + ? null + : StringFilter.fromJson(json['origName'] as Map), + tags: json['tags'] == null + ? null + : TagFilter.fromJson(json['tags'] as Map), + albums: json['albums'] == null + ? null + : AlbumFilter.fromJson(json['albums'] as Map), + ); +} + +Map _$MediaItemFilterToJson(MediaItemFilter instance) => + { + 'id': instance.id?.toJson(), + 'createdAt': instance.createdAt?.toJson(), + 'updatedAt': instance.updatedAt?.toJson(), + 'exifDate': instance.exifDate?.toJson(), + 'latitude': instance.latitude?.toJson(), + 'longitude': instance.longitude?.toJson(), + 'isVideo': instance.isVideo?.toJson(), + 'origName': instance.origName?.toJson(), + 'tags': instance.tags?.toJson(), + 'albums': instance.albums?.toJson(), + }; + +TagFilter _$TagFilterFromJson(Map json) { + return TagFilter( + id: json['id'] == null + ? null + : IDFilter.fromJson(json['id'] as Map), + createdAt: json['createdAt'] == null + ? null + : TimeFilter.fromJson(json['createdAt'] as Map), + updatedAt: json['updatedAt'] == null + ? null + : TimeFilter.fromJson(json['updatedAt'] as Map), + name: json['name'] == null + ? null + : StringFilter.fromJson(json['name'] as Map), + ); +} + +Map _$TagFilterToJson(TagFilter instance) => { + 'id': instance.id?.toJson(), + 'createdAt': instance.createdAt?.toJson(), + 'updatedAt': instance.updatedAt?.toJson(), + 'name': instance.name?.toJson(), + }; + +AlbumFilter _$AlbumFilterFromJson(Map json) { + return AlbumFilter( + id: json['id'] == null + ? null + : IDFilter.fromJson(json['id'] as Map), + createdAt: json['createdAt'] == null + ? null + : TimeFilter.fromJson(json['createdAt'] as Map), + updatedAt: json['updatedAt'] == null + ? null + : TimeFilter.fromJson(json['updatedAt'] as Map), + name: json['name'] == null + ? null + : StringFilter.fromJson(json['name'] as Map), + ); +} + +Map _$AlbumFilterToJson(AlbumFilter instance) => + { + 'id': instance.id?.toJson(), + 'createdAt': instance.createdAt?.toJson(), + 'updatedAt': instance.updatedAt?.toJson(), + 'name': instance.name?.toJson(), + }; + +Page _$PageFromJson(Map json) { + return Page( + size: json['size'] as int, + page: json['page'] as int, + ); +} + +Map _$PageToJson(Page instance) => { + 'size': instance.size, + 'page': instance.page, + }; + +Order _$OrderFromJson(Map json) { + return Order( + by: json['by'] as String, + direction: _$enumDecodeNullable(_$OrderDirectionEnumMap, json['direction'], + unknownValue: OrderDirection.artemisUnknown), + ); +} + +Map _$OrderToJson(Order instance) => { + 'by': instance.by, + 'direction': _$OrderDirectionEnumMap[instance.direction], + }; + +const _$OrderDirectionEnumMap = { + OrderDirection.asc: 'ASC', + OrderDirection.desc: 'DESC', + OrderDirection.artemisUnknown: 'ARTEMIS_UNKNOWN', +}; + LoginArguments _$LoginArgumentsFromJson(Map json) { return LoginArguments( user: json['user'] as String, @@ -219,3 +531,25 @@ Map _$CreateMediaItemArgumentsToJson( { 'file': fromDartMultipartFileToGraphQLUpload(instance.file), }; + +MediaItemsArguments _$MediaItemsArgumentsFromJson(Map json) { + return MediaItemsArguments( + order: json['order'] == null + ? null + : Order.fromJson(json['order'] as Map), + page: json['page'] == null + ? null + : Page.fromJson(json['page'] as Map), + filter: json['filter'] == null + ? null + : MediaItemFilter.fromJson(json['filter'] as Map), + ); +} + +Map _$MediaItemsArgumentsToJson( + MediaItemsArguments instance) => + { + 'order': instance.order?.toJson(), + 'page': instance.page?.toJson(), + 'filter': instance.filter?.toJson(), + }; diff --git a/web_native/lib/screens/login_screen.dart b/web_native/lib/screens/login_screen.dart index ab2a573..34a29ff 100644 --- a/web_native/lib/screens/login_screen.dart +++ b/web_native/lib/screens/login_screen.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; - import 'package:fluro/fluro.dart'; + import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:imagini/blocs/login_bloc.dart'; import 'package:imagini/core/app_provider.dart'; class LoginScreen extends StatefulWidget { - static const String PATH = '/Login'; + static const String PATH = '/'; LoginScreen({Key key}) : super(key: key); @@ -16,7 +17,7 @@ class LoginScreen extends StatefulWidget { class _LoginScreenState extends State { - // LoginBloc bloc; + LoginBloc bloc; @override void dispose() { @@ -29,12 +30,76 @@ class _LoginScreenState extends State { _init(); return Scaffold( - body: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 500), - child: Container( - margin: EdgeInsets.fromLTRB(50, 0, 50, 0), - height: 500, + body: StreamBuilder( + stream: bloc.authenticatedResult, + builder: (context, snapshot) { + if (snapshot.data == null || snapshot.data == true) + return _appLoading(); + return _appLogin(); + } + ) + ); + } + + void _init(){ + if(bloc != null){ + return; + } + + bloc = LoginBloc(AppProvider.getApplication(context)); + bloc.authenticatedResult.listen((bool status) { + if (status) + AppProvider.getRouter(context).navigateTo(context, "/Home", transition: TransitionType.fadeIn); + }); + + bloc.loginResult.listen((bool status) { + if (status == null || status == false) + return; + + AppProvider.getRouter(context).navigateTo(context, "/Home", transition: TransitionType.fadeIn); + }); + } + + Widget _appLoading(){ + return Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 500), + child: Container( + margin: EdgeInsets.fromLTRB(50, 0, 50, 0), + height: 270, + child: Column( + children: [ + Container( + child: FittedBox( + fit: BoxFit.contain, + child: const FlutterLogo(), + ), + width: 175, + margin: EdgeInsets.fromLTRB(0, 0, 0, 50), + ), + PlatformCircularProgressIndicator() + ], + ), + ), + ), + ); + } + + Widget _appLogin(){ + TextEditingController serverController = new TextEditingController(); + TextEditingController userController = new TextEditingController(); + TextEditingController passwordController = new TextEditingController(); + + final _formKey = GlobalKey(); + + return Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 500), + child: Container( + margin: EdgeInsets.fromLTRB(50, 0, 50, 0), + height: 500, + child: Form( + key: _formKey, child: Column( children: [ Container( @@ -46,24 +111,45 @@ class _LoginScreenState extends State { margin: EdgeInsets.fromLTRB(0, 0, 0, 50), ), Expanded( - child: TextField( + child: TextFormField( + controller: serverController, + validator: (value) { + if (value.isEmpty) { + return 'Please enter server address'; + } + return null; + }, decoration: InputDecoration( labelText: 'Server Address' ), ), ), Expanded( - child: TextField( + child: TextFormField( + controller: userController, + validator: (value) { + if (value.isEmpty) { + return 'Please enter username or email'; + } + return null; + }, decoration: InputDecoration( labelText: 'Username / Email' ), ), ), Expanded( - child: TextField( + child: TextFormField( obscureText: true, enableSuggestions: false, autocorrect: false, + controller: passwordController, + validator: (value) { + if (value.isEmpty) { + return 'Please enter password'; + } + return null; + }, decoration: InputDecoration( labelText: 'Password' ), @@ -73,11 +159,31 @@ class _LoginScreenState extends State { width: double.infinity, child: PlatformButton( onPressed: () { - AppProvider.getRouter(context).navigateTo(context, "/Home", transition: TransitionType.fadeIn); + if (!_formKey.currentState.validate()) + return; + bloc.attemptLogin(userController.text, passwordController.text, serverController.text); }, child: Text('Login') ), ), + + StreamBuilder( + stream: bloc.loginResult, + builder: (context, snapshot) { + if (snapshot.data == null || snapshot.data == true) + return Container(); + + return Expanded( + child: Padding( + padding: EdgeInsets.fromLTRB(20, 20, 20, 20), + child: Text( + "Login Failed", + style: TextStyle(color: Colors.red), + ) + ), + ); + } + ) ], ), ), @@ -85,10 +191,4 @@ class _LoginScreenState extends State { ), ); } - - void _init(){ - // if(null == bloc){ - // bloc = LoginBloc(AppProvider.getApplication(context)); - // } - } } diff --git a/web_native/lib/screens/splash_screen.dart b/web_native/lib/screens/splash_screen.dart deleted file mode 100644 index ba5bcbe..0000000 --- a/web_native/lib/screens/splash_screen.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:fluro/fluro.dart'; - -import 'package:imagini/core/app_provider.dart'; -import 'package:imagini/blocs/splash_bloc.dart'; -import 'package:imagini/graphql/imagini_graphql.dart'; - -class SplashScreen extends StatefulWidget { - static const String PATH = '/'; - - SplashScreen({Key key}) : super(key: key); - - @override - _SplashScreenState createState() => _SplashScreenState(); -} - -class _SplashScreenState extends State { - - SplashBloc bloc; - - @override - void dispose() { - super.dispose(); - bloc.dispose(); - } - - @override - Widget build(BuildContext context) { - _init(); - - return Scaffold( - body: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 500), - child: Container( - margin: EdgeInsets.fromLTRB(50, 0, 50, 0), - height: 270, - child: Column( - children: [ - Container( - child: FittedBox( - fit: BoxFit.contain, - child: const FlutterLogo(), - ), - width: 175, - margin: EdgeInsets.fromLTRB(0, 0, 0, 50), - ), - PlatformCircularProgressIndicator() - ], - ), - ), - ), - ), - ); - } - - void _init(){ - if(null == bloc){ - bloc = SplashBloc(AppProvider.getApplication(context)); - bloc.loginResult.listen((Login$Query$AuthResponse lr) { - if (lr.result == AuthResult.success) { - AppProvider.getRouter(context).navigateTo(context, "/Home", transition: TransitionType.fadeIn); - } else { - AppProvider.getRouter(context).navigateTo(context, "/Login", transition: TransitionType.fadeIn); - } - }); - } - } -}