feat: add onboarding functionality and theme switch button

This commit is contained in:
Udhay-Adithya
2025-03-17 17:30:48 +05:30
parent b5d4922045
commit b64d453886
11 changed files with 340 additions and 32 deletions

1
assets/api_server.json Normal file

File diff suppressed because one or more lines are too long

1
assets/files.json Normal file

File diff suppressed because one or more lines are too long

1
assets/generate.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -13,6 +13,9 @@ const kAssetIntroMd = "assets/intro.md";
const kAssetSendingLottie = "assets/sending.json"; const kAssetSendingLottie = "assets/sending.json";
const kAssetSavingLottie = "assets/saving.json"; const kAssetSavingLottie = "assets/saving.json";
const kAssetSavedLottie = "assets/completed.json"; const kAssetSavedLottie = "assets/completed.json";
const kAssetGenerateCodeLottie = "assets/generate.json";
const kAssetApiServerLottie = "assets/api_server.json";
const kAssetFolderLottie = "assets/files.json";
final kIsMacOS = !kIsWeb && Platform.isMacOS; final kIsMacOS = !kIsWeb && Platform.isMacOS;
final kIsWindows = !kIsWeb && Platform.isWindows; final kIsWindows = !kIsWeb && Platform.isWindows;

View File

@ -0,0 +1,29 @@
import 'package:apidash/providers/settings_providers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ThemeSwitchButton extends StatelessWidget {
const ThemeSwitchButton({super.key});
@override
Widget build(BuildContext context) {
return Consumer(builder: (context, ref, child) {
final settings = ref.watch(settingsProvider);
return IconButton(
icon: settings.isDark
? const Icon(
Icons.dark_mode_rounded,
color: Colors.indigo,
)
: const Icon(
Icons.light_mode_rounded,
color: Colors.yellow,
),
onPressed: () {
ref.read(settingsProvider.notifier).update(isDark: !settings.isDark);
},
);
});
}
}

View File

@ -1,3 +1,5 @@
import 'package:apidash/screens/mobile/onboarding_screen.dart';
import 'package:apidash/services/shared_preferences_services.dart';
import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -19,41 +21,50 @@ class MobileDashboard extends ConsumerStatefulWidget {
} }
class _MobileDashboardState extends ConsumerState<MobileDashboard> { class _MobileDashboardState extends ConsumerState<MobileDashboard> {
@override Future<bool> _checkOnboardingStatus() async {
Widget build( return await getOnboardingStatusFromSharedPrefs();
BuildContext context, }
) {
final railIdx = ref.watch(navRailIndexStateProvider);
final isLeftDrawerOpen = ref.watch(leftDrawerStateProvider);
return AnnotatedRegion<SystemUiOverlayStyle>( @override
value: FlexColorScheme.themedSystemNavigationBar( Widget build(BuildContext context) {
context, return FutureBuilder<bool>(
opacity: 0, future: _checkOnboardingStatus(),
noAppBar: true, builder: (context, snapshot) {
), final railIdx = ref.watch(navRailIndexStateProvider);
child: Stack( final isLeftDrawerOpen = ref.watch(leftDrawerStateProvider);
alignment: AlignmentDirectional.bottomCenter, if (snapshot.connectionState == ConnectionState.waiting) {
children: [ return const Center(child: CircularProgressIndicator());
PageBranch( } else if (snapshot.data == false) {
pageIndex: railIdx, return const OnboardingScreen();
}
return AnnotatedRegion<SystemUiOverlayStyle>(
value: FlexColorScheme.themedSystemNavigationBar(
context,
opacity: 0,
noAppBar: true,
), ),
if (context.isMediumWindow) child: Stack(
AnimatedPositioned( alignment: AlignmentDirectional.bottomCenter,
bottom: railIdx > 2 children: [
? 0 PageBranch(pageIndex: railIdx),
: isLeftDrawerOpen if (context.isMediumWindow)
AnimatedPositioned(
bottom: railIdx > 2
? 0 ? 0
: -(72 + MediaQuery.paddingOf(context).bottom), : isLeftDrawerOpen
left: 0, ? 0
right: 0, : -(72 + MediaQuery.paddingOf(context).bottom),
height: 70 + MediaQuery.paddingOf(context).bottom, left: 0,
duration: const Duration(milliseconds: 200), right: 0,
curve: Curves.easeOut, height: 70 + MediaQuery.paddingOf(context).bottom,
child: const BottomNavBar(), duration: const Duration(milliseconds: 200),
), curve: Curves.easeOut,
], child: const BottomNavBar(),
), ),
],
),
);
},
); );
} }
} }

View File

@ -0,0 +1,170 @@
import 'package:apidash/consts.dart';
import 'package:apidash/screens/common_widgets/theme_switch_button.dart';
import 'package:apidash/screens/mobile/widgets/onboarding_slide.dart';
import 'package:apidash/screens/screens.dart';
import 'package:apidash/services/services.dart';
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
class OnboardingScreen extends StatefulWidget {
const OnboardingScreen({super.key});
@override
State<OnboardingScreen> createState() => _OnboardingScreenState();
}
class _OnboardingScreenState extends State<OnboardingScreen> {
int currentPageIndex = 0;
final CarouselSliderController _carouselController =
CarouselSliderController();
void _onNextPressed() {
if (currentPageIndex < 2) {
_carouselController.nextPage(
duration: const Duration(milliseconds: 600),
curve: Curves.ease,
);
} else {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) => MobileDashboard(),
),
(route) => false,
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.surface,
actions: [
const ThemeSwitchButton(),
TextButton(
onPressed: () async {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) => MobileDashboard(),
),
(route) => false,
);
await setOnboardingStatusToSharedPrefs(
isOnboardingComplete: true,
);
},
child: const Text(
'Skip',
),
),
],
),
body: Container(
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
Expanded(
child: CarouselSlider(
carouselController: _carouselController,
options: CarouselOptions(
height: MediaQuery.of(context).size.height * 0.75,
viewportFraction: 1.0,
enableInfiniteScroll: false,
onPageChanged: (index, reason) {
setState(() {
currentPageIndex = index;
});
},
),
items: [
OnboardingSlide(
context: context,
assetPath: kAssetApiServerLottie,
assetSize: context.width * 0.75,
title: "Test APIs with Ease",
description:
"Send requests, preview responses, and test APIs with ease. REST and GraphQL support included!",
),
OnboardingSlide(
context: context,
assetPath: kAssetFolderLottie,
assetSize: context.width * 0.55,
title: "Organize & Save Requests",
description:
"Save and organize API requests into collections for quick access and better workflow.",
),
OnboardingSlide(
context: context,
assetPath: kAssetGenerateCodeLottie,
assetSize: context.width * 0.65,
title: "Generate Code Instantly",
description:
"Integrate APIs using well tested code generators for JavaScript, Python, Dart, Kotlin & others.",
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left: 36.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(3, (index) {
bool isSelected = currentPageIndex == index;
return GestureDetector(
onTap: () {
_carouselController.animateToPage(index);
},
child: AnimatedContainer(
width: isSelected ? 40 : 18,
height: 7,
margin: const EdgeInsets.symmetric(horizontal: 3),
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context)
.colorScheme
.secondaryContainer,
borderRadius: BorderRadius.circular(9),
),
duration: const Duration(milliseconds: 300),
),
);
}),
),
),
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: IconButton(
onPressed: () async {
_onNextPressed();
await setOnboardingStatusToSharedPrefs(
isOnboardingComplete: true,
);
},
icon: const Icon(
Icons.arrow_forward_rounded,
size: 30,
),
style: IconButton.styleFrom(
elevation: 8,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(35),
),
),
),
),
],
),
const SizedBox(height: 60),
],
),
),
);
}
}

View File

@ -0,0 +1,70 @@
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
class OnboardingSlide extends StatelessWidget {
final BuildContext context;
final String assetPath;
final double assetSize;
final String title;
final String description;
const OnboardingSlide({
required this.context,
required this.assetPath,
required this.assetSize,
required this.title,
required this.description,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(top: 75.0),
child: Center(
child: Lottie.asset(
assetPath,
renderCache: RenderCache.drawingCommands,
width: assetSize,
fit: BoxFit.cover,
),
),
),
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16,
),
child: Text(
description,
textAlign: TextAlign.center,
style: kTextStyleButton.copyWith(fontSize: 16),
),
),
const SizedBox(
height: 70,
)
],
),
],
);
}
}

View File

@ -3,6 +3,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../models/models.dart'; import '../models/models.dart';
const String kSharedPrefSettingsKey = 'apidash-settings'; const String kSharedPrefSettingsKey = 'apidash-settings';
const String kSharedPrefOnboardingKey = 'apidash-onboard-status';
Future<SettingsModel?> getSettingsFromSharedPrefs() async { Future<SettingsModel?> getSettingsFromSharedPrefs() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@ -22,6 +23,18 @@ Future<void> setSettingsToSharedPrefs(SettingsModel settingsModel) async {
await prefs.setString(kSharedPrefSettingsKey, settingsModel.toString()); await prefs.setString(kSharedPrefSettingsKey, settingsModel.toString());
} }
Future<void> setOnboardingStatusToSharedPrefs(
{required bool isOnboardingComplete}) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(kSharedPrefOnboardingKey, isOnboardingComplete);
}
Future<bool> getOnboardingStatusFromSharedPrefs() async {
final prefs = await SharedPreferences.getInstance();
final bool? onboardingStatus = prefs.getBool(kSharedPrefOnboardingKey);
return onboardingStatus ?? false;
}
Future<void> clearSharedPrefs() async { Future<void> clearSharedPrefs() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.remove(kSharedPrefSettingsKey); await prefs.remove(kSharedPrefSettingsKey);

View File

@ -175,6 +175,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.9.4" version: "8.9.4"
carousel_slider:
dependency: "direct main"
description:
name: carousel_slider
sha256: "7b006ec356205054af5beaef62e2221160ea36b90fb70a35e4deacd49d0349ae"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
characters: characters:
dependency: transitive dependency: transitive
description: description:

View File

@ -69,6 +69,7 @@ dependencies:
git: git:
url: https://github.com/google/flutter-desktop-embedding.git url: https://github.com/google/flutter-desktop-embedding.git
path: plugins/window_size path: plugins/window_size
carousel_slider: ^5.0.0
dependency_overrides: dependency_overrides:
extended_text_field: ^16.0.0 extended_text_field: ^16.0.0