diff --git a/assets/success.png b/assets/success.png new file mode 100644 index 00000000..65cdba35 Binary files /dev/null and b/assets/success.png differ diff --git a/lib/components/Login/Login.dart b/lib/components/Login/Login.dart new file mode 100644 index 00000000..42b82055 --- /dev/null +++ b/lib/components/Login/Login.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/components/Login/LoginForm.dart'; +import 'package:spotube/components/Shared/Hyperlink.dart'; +import 'package:spotube/components/Shared/LinkText.dart'; +import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; +import 'package:spotube/hooks/useBreakpoints.dart'; +import 'package:spotube/models/Logger.dart'; + +class Login extends HookConsumerWidget { + Login({Key? key}) : super(key: key); + final log = getLogger(Login); + + @override + Widget build(BuildContext context, ref) { + final breakpoint = useBreakpoints(); + + final textTheme = Theme.of(context).textTheme; + + return Scaffold( + appBar: const PageWindowTitleBar(leading: BackButton()), + body: SingleChildScrollView( + child: Center( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + children: [ + Image.asset( + "assets/spotube-logo.png", + width: MediaQuery.of(context).size.width * + (breakpoint <= Breakpoints.md ? .5 : .3), + ), + Text("Add your spotify credentials to get started", + style: breakpoint <= Breakpoints.md + ? textTheme.headline5 + : textTheme.headline4), + Text( + "Don't worry, any of your credentials won't be collected or shared with anyone", + style: Theme.of(context).textTheme.caption, + ), + const SizedBox(height: 10), + LoginForm( + onDone: () => GoRouter.of(context).pop(), + ), + const SizedBox(height: 10), + Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + const Text("Don't know how to do this?"), + TextButton( + child: const Text( + "Follow along the Step by Step guid", + ), + onPressed: () => GoRouter.of(context).push( + "/login-tutorial", + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/components/Login/LoginForm.dart b/lib/components/Login/LoginForm.dart new file mode 100644 index 00000000..3131ec8f --- /dev/null +++ b/lib/components/Login/LoginForm.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/helpers/oauth-login.dart'; +import 'package:spotube/models/Logger.dart'; +import 'package:spotube/provider/Auth.dart'; + +class LoginForm extends HookConsumerWidget { + final void Function()? onDone; + LoginForm({this.onDone, Key? key}) : super(key: key); + + final log = getLogger(LoginForm); + + @override + Widget build(BuildContext context, ref) { + Auth authState = ref.watch(authProvider); + final clientIdController = useTextEditingController(); + final clientSecretController = useTextEditingController(); + final fieldError = useState(false); + + Future handleLogin(Auth authState) async { + try { + if (clientIdController.value.text == "" || + clientSecretController.value.text == "") { + fieldError.value = true; + } + await oauthLogin( + ref.read(authProvider), + clientId: clientIdController.value.text, + clientSecret: clientSecretController.value.text, + ).then( + (value) => onDone?.call(), + ); + } catch (e) { + log.e("[Login.handleLogin] $e"); + } + } + + return ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400, + ), + child: Column( + children: [ + TextField( + controller: clientIdController, + decoration: const InputDecoration( + hintText: "Spotify Client ID", + label: Text("ClientID"), + ), + keyboardType: TextInputType.visiblePassword, + ), + const SizedBox(height: 10), + TextField( + controller: clientSecretController, + decoration: const InputDecoration( + hintText: "Spotify Client Secret", + label: Text("Client Secret"), + ), + keyboardType: TextInputType.visiblePassword, + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () async { + await handleLogin(authState); + }, + child: const Text("Submit"), + ) + ], + ), + ); + } +} diff --git a/lib/components/Login/LoginTutorial.dart b/lib/components/Login/LoginTutorial.dart new file mode 100644 index 00000000..5a4ccc07 --- /dev/null +++ b/lib/components/Login/LoginTutorial.dart @@ -0,0 +1,180 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:introduction_screen/introduction_screen.dart'; +import 'package:spotube/components/Login/LoginForm.dart'; +import 'package:spotube/components/Shared/Hyperlink.dart'; +import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; +import 'package:spotube/provider/Auth.dart'; + +class LoginTutorial extends ConsumerWidget { + const LoginTutorial({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final auth = ref.watch(authProvider); + + return Scaffold( + appBar: PageWindowTitleBar( + leading: TextButton( + child: const Text("Exit"), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + body: IntroductionScreen( + next: const Text("Next"), + back: const Text("Previous"), + showBackButton: true, + overrideDone: TextButton( + child: const Text("Done"), + onPressed: auth.isLoggedIn + ? () { + GoRouter.of(context).go("/"); + } + : null, + ), + pages: [ + PageViewModel( + title: "Step 1", + image: CachedNetworkImage( + imageUrl: + "https://user-images.githubusercontent.com/61944859/111762106-d1d37680-88ca-11eb-9884-ec7a40c0dd27.png"), + bodyWidget: Wrap( + children: [ + Text("First, Go to ", + style: Theme.of(context).textTheme.bodyText1), + Hyperlink( + "developer.spotify.com/dashboard ", + "https://developer.spotify.com/dashboard", + style: Theme.of(context).textTheme.bodyText1!, + ), + Text( + "and Login if you're not logged in", + style: Theme.of(context).textTheme.bodyText1, + ), + ], + ), + ), + PageViewModel( + title: "Step 2", + image: CachedNetworkImage( + imageUrl: + "https://user-images.githubusercontent.com/61944859/111762507-473f4700-88cb-11eb-91f3-d480e9584883.png"), + bodyWidget: Text( + "Now, create an Spotify Developer Application by Clicking on the \"CREATE AN APP\" button. Give it a name and description too", + textAlign: TextAlign.left, + style: Theme.of(context).textTheme.bodyText1, + ), + ), + PageViewModel( + title: "Step 3 [Really Important!]", + bodyWidget: Column( + children: [ + Text( + "Tap on the \"EDIT SETTINGS\" Button & navigate to \"Redirect URIs\" section", + textAlign: TextAlign.left, + style: Theme.of(context).textTheme.bodyText1, + ), + const SizedBox(height: 10), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + "Add", + style: Theme.of(context).textTheme.bodyText1, + ), + TextButton( + child: Text( + "http://localhost:4304/auth/spotify/callback", + style: Theme.of(context).textTheme.bodyText1?.copyWith( + color: Theme.of(context).primaryColor, + ), + ), + onPressed: () async { + await Clipboard.setData( + const ClipboardData( + text: "http://localhost:4304/auth/spotify/callback", + ), + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + width: 300, + behavior: SnackBarBehavior.floating, + content: Text( + "Copied http://localhost:4304/auth/spotify/callback to clipboard", + textAlign: TextAlign.center, + ), + ), + ); + }, + ), + Text( + "to \"Redirect URIs\"", + style: Theme.of(context).textTheme.bodyText1, + ), + ], + ), + const SizedBox(height: 10), + Wrap( + runSpacing: 10, + spacing: 10, + children: [ + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: CachedNetworkImage( + imageUrl: + "https://user-images.githubusercontent.com/61944859/172991668-fa40f247-1118-4aba-a749-e669b732fa4d.jpg", + ), + ), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 700), + child: CachedNetworkImage( + imageUrl: + "https://user-images.githubusercontent.com/61944859/111768971-d308a180-88d2-11eb-9108-3e7444cef049.png", + ), + ), + ], + ), + ], + ), + ), + PageViewModel( + title: "Step 4", + image: CachedNetworkImage( + imageUrl: + "https://user-images.githubusercontent.com/61944859/111769501-7fe31e80-88d3-11eb-8fc1-f3655dbd4711.png"), + body: + "Finally, reveal the \"Client Secret\" by clicking on the \"SHOW CLIENT SECRET\" text\n Copy the Client ID & Client Secret then Paste them in the next Screen", + ), + PageViewModel( + title: "Step 5", + bodyWidget: Column( + children: [ + Text( + "Paste the Copied \"Client ID\" and \"Client Secret\" Here", + style: Theme.of(context).textTheme.bodyText1, + ), + const SizedBox(height: 10), + LoginForm(), + ], + ), + ), + if (auth.isLoggedIn) + PageViewModel( + decoration: const PageDecoration( + bodyAlignment: Alignment.center, + ), + title: "Success🥳", + image: Image.asset("assets/success.png"), + body: + "Now you're successfully Logged In with your Spotify account. Good Job, mate!", + ), + ], + ), + ); + } +} diff --git a/lib/components/Settings/Login.dart b/lib/components/Settings/Login.dart deleted file mode 100644 index a66e4e8f..00000000 --- a/lib/components/Settings/Login.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:go_router/go_router.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/components/Shared/Hyperlink.dart'; -import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; -import 'package:spotube/helpers/oauth-login.dart'; -import 'package:spotube/hooks/useBreakpoints.dart'; -import 'package:spotube/models/Logger.dart'; -import 'package:spotube/provider/Auth.dart'; - -class Login extends HookConsumerWidget { - Login({Key? key}) : super(key: key); - final log = getLogger(Login); - - @override - Widget build(BuildContext context, ref) { - Auth authState = ref.watch(authProvider); - final clientIdController = useTextEditingController(); - final clientSecretController = useTextEditingController(); - final fieldError = useState(false); - final breakpoint = useBreakpoints(); - - Future handleLogin(Auth authState) async { - try { - if (clientIdController.value.text == "" || - clientSecretController.value.text == "") { - fieldError.value = true; - } - await oauthLogin( - ref.read(authProvider), - clientId: clientIdController.value.text, - clientSecret: clientSecretController.value.text, - ).then( - (value) => GoRouter.of(context).pop(), - ); - } catch (e) { - log.e("[Login.handleLogin] $e"); - } - } - - final textTheme = Theme.of(context).textTheme; - - return Scaffold( - appBar: const PageWindowTitleBar(leading: BackButton()), - body: SingleChildScrollView( - child: Center( - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 10), - child: Column( - children: [ - Image.asset( - "assets/spotube-logo.png", - width: MediaQuery.of(context).size.width * - (breakpoint <= Breakpoints.md ? .5 : .3), - ), - Text("Add your spotify credentials to get started", - style: breakpoint <= Breakpoints.md - ? textTheme.headline5 - : textTheme.headline4), - const Text( - "Don't worry, any of your credentials won't be collected or shared with anyone"), - const Hyperlink("How to get these client-id & client-secret?", - "https://github.com/KRTirtho/spotube#optional-configurations"), - const SizedBox( - height: 10, - ), - Container( - constraints: const BoxConstraints( - maxWidth: 400, - ), - child: Column( - children: [ - TextField( - controller: clientIdController, - decoration: const InputDecoration( - hintText: "Spotify Client ID", - label: Text("ClientID"), - ), - keyboardType: TextInputType.visiblePassword, - ), - const SizedBox(height: 10), - TextField( - controller: clientSecretController, - decoration: const InputDecoration( - hintText: "Spotify Client Secret", - label: Text("Client Secret"), - ), - keyboardType: TextInputType.visiblePassword, - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () async { - await handleLogin(authState); - }, - child: const Text("Submit"), - ) - ], - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/components/Shared/Hyperlink.dart b/lib/components/Shared/Hyperlink.dart index d4a70b1c..fc121951 100644 --- a/lib/components/Shared/Hyperlink.dart +++ b/lib/components/Shared/Hyperlink.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:spotube/components/Shared/AnchorButton.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class Hyperlink extends StatelessWidget { final String text; @@ -22,7 +23,7 @@ class Hyperlink extends StatelessWidget { return AnchorButton( text, onTap: () async { - await launch(url); + await launchUrlString(url); }, key: key, overflow: overflow, diff --git a/lib/models/GoRouteDeclarations.dart b/lib/models/GoRouteDeclarations.dart index 894a1d97..cb28a97f 100644 --- a/lib/models/GoRouteDeclarations.dart +++ b/lib/models/GoRouteDeclarations.dart @@ -4,7 +4,8 @@ import 'package:spotube/components/Album/AlbumView.dart'; import 'package:spotube/components/Artist/ArtistAlbumView.dart'; import 'package:spotube/components/Artist/ArtistProfile.dart'; import 'package:spotube/components/Home/Home.dart'; -import 'package:spotube/components/Settings/Login.dart'; +import 'package:spotube/components/Login/Login.dart'; +import 'package:spotube/components/Login/LoginTutorial.dart'; import 'package:spotube/components/Player/PlayerView.dart'; import 'package:spotube/components/Playlist/PlaylistView.dart'; import 'package:spotube/components/Settings/Settings.dart'; @@ -22,6 +23,12 @@ GoRouter createGoRouter() => GoRouter( child: Login(), ), ), + GoRoute( + path: "/login-tutorial", + pageBuilder: (context, state) => const SpotubePage( + child: LoginTutorial(), + ), + ), GoRoute( path: "/settings", pageBuilder: (context, state) => const SpotubePage( @@ -71,6 +78,6 @@ GoRouter createGoRouter() => GoRouter( child: PlayerView(), ); }, - ) + ), ], ); diff --git a/pubspec.lock b/pubspec.lock index 4374a9f0..70e08e12 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,7 +37,7 @@ packages: source: hosted version: "2.3.1" async: - dependency: "direct main" + dependency: transitive description: name: async url: "https://pub.dartlang.org" @@ -323,6 +323,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.7.3" + dots_indicator: + dependency: transitive + description: + name: dots_indicator + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" fading_edge_scrollview: dependency: transitive description: @@ -527,6 +534,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.2.0" + introduction_screen: + dependency: "direct main" + description: + name: introduction_screen + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" io: dependency: transitive description: @@ -828,13 +842,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" - resizable_widget: - dependency: "direct main" - description: - name: resizable_widget - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.5" riverpod: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4c4b00ab..66304f37 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,6 +63,7 @@ dependencies: hive_flutter: ^1.1.0 dbus: ^0.7.3 audioplayers: ^1.0.1 + introduction_screen: ^3.0.2 dev_dependencies: flutter_test: