✨ feat(ui): add onboarding tutorial for new users

This commit is contained in:
sankalp committed 2025-07-03 23:30:53 +05:45
1 parent 41e6fec258
commit 4b6035ee02
8 files changed
+354

No files matched your search

@@ -89,6 +89,7 @@ import io.github.sankalp.lunox.dialogs.FrozenAppsDialogs;
import io.github.sankalp.lunox.dialogs.GlobalColorSizeDialog; import io.github.sankalp.lunox.dialogs.GlobalColorSizeDialog;
import io.github.sankalp.lunox.dialogs.GlobalSettingsDialog; import io.github.sankalp.lunox.dialogs.GlobalSettingsDialog;
import io.github.sankalp.lunox.dialogs.HiddenAppsDialogs; import io.github.sankalp.lunox.dialogs.HiddenAppsDialogs;
import io.github.sankalp.lunox.dialogs.OnboardingTutorialDialog;
import io.github.sankalp.lunox.dialogs.PaddingDialog; import io.github.sankalp.lunox.dialogs.PaddingDialog;
import io.github.sankalp.lunox.dialogs.RenameInputDialogs; import io.github.sankalp.lunox.dialogs.RenameInputDialogs;
import io.github.sankalp.lunox.model.Apps; import io.github.sankalp.lunox.model.Apps;
@@ -314,6 +315,9 @@ public class LauncherActivity extends Activity implements View.OnClickListener,
setDefaultButton = findViewById(R.id.set_default_button); setDefaultButton = findViewById(R.id.set_default_button);
setDefaultButton.setOnClickListener(this); setDefaultButton.setOnClickListener(this);
// Show onboarding tutorial for first-time users
showOnboardingTutorialIfNeeded();
} }
private void setSearchBoxListeners() { private void setSearchBoxListeners() {
@@ -1343,6 +1347,21 @@ public class LauncherActivity extends Activity implements View.OnClickListener,
/** /**
* Show detailed double tap to lock dialog * Show detailed double tap to lock dialog
*/ */
private void showOnboardingTutorialIfNeeded() {
if (DbUtils.isFirstTimeUser() && !DbUtils.isTutorialCompleted()) {
// Use a post to ensure the UI is fully loaded before showing the dialog
mHomeLayout.post(() -> {
OnboardingTutorialDialog tutorialDialog = new OnboardingTutorialDialog(this, this);
tutorialDialog.show();
});
}
}
public void showOnboardingTutorial() {
OnboardingTutorialDialog tutorialDialog = new OnboardingTutorialDialog(this, this);
tutorialDialog.show();
}
private void showDoubleTapLockDialog() { private void showDoubleTapLockDialog() {
showDialog(new DoubleTapLockDialog(this, this), true); showDialog(new DoubleTapLockDialog(this, this), true);
} }
@@ -91,6 +91,7 @@ public class GlobalSettingsDialog extends Dialog implements View.OnClickListener
findViewById(R.id.settings_padding).setOnClickListener(this); findViewById(R.id.settings_padding).setOnClickListener(this);
findViewById(R.id.settings_color_size).setOnClickListener(this); findViewById(R.id.settings_color_size).setOnClickListener(this);
findViewById(R.id.settings_sort_app_by).setOnClickListener(this); findViewById(R.id.settings_sort_app_by).setOnClickListener(this);
findViewById(R.id.settings_tutorial).setOnClickListener(this);
findViewById(R.id.settings_restart_launcher).setOnClickListener(this); findViewById(R.id.settings_restart_launcher).setOnClickListener(this);
//TODO: remove this var //TODO: remove this var
@@ -159,6 +160,8 @@ public class GlobalSettingsDialog extends Dialog implements View.OnClickListener
} else if (id == R.id.settings_padding) { } else if (id == R.id.settings_padding) {
launcherActivity.closeGlobalSettingsDialog(); launcherActivity.closeGlobalSettingsDialog();
launcherActivity.setPadding(); launcherActivity.setPadding();
} else if (id == R.id.settings_tutorial) {
showTutorial();
} else if (id == R.id.settings_restart_launcher) { } else if (id == R.id.settings_restart_launcher) {
launcherActivity.recreate(); launcherActivity.recreate();
} else if (id == R.id.settings_double_tap_lock) { } else if (id == R.id.settings_double_tap_lock) {
@@ -428,4 +431,9 @@ public class GlobalSettingsDialog extends Dialog implements View.OnClickListener
} }
} }
private void showTutorial() {
launcherActivity.closeGlobalSettingsDialog();
launcherActivity.showOnboardingTutorial();
}
} }
@@ -0,0 +1,155 @@
/*
* Lunox
* Copyright (C) 2025 Sankalp Tharu
*
* This file is part of Last Launcher (forked).
* Licensed under the GNU General Public License v3 or later.
* See <http://www.gnu.org/licenses/>.
*
*/
package io.github.sankalp.lunox.dialogs;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.widget.TextView;
import io.github.sankalp.lunox.LauncherActivity;
import io.github.sankalp.lunox.R;
import io.github.sankalp.lunox.utils.DbUtils;
/**
* Dialog for showing onboarding tutorial to new users
*/
public class OnboardingTutorialDialog extends Dialog implements View.OnClickListener {
private final LauncherActivity launcherActivity;
private final Context context;
private TextView titleView;
private TextView stepIndicatorView;
private TextView contentView;
private TextView skipButton;
private TextView previousButton;
private TextView nextButton;
private int currentStep = 1;
private static final int TOTAL_STEPS = 7;
// Tutorial step data
private final int[] stepTitles = {
R.string.tutorial_step_1_title,
R.string.tutorial_step_2_title,
R.string.tutorial_step_3_title,
R.string.tutorial_step_4_title,
R.string.tutorial_step_5_title,
R.string.tutorial_step_6_title,
R.string.tutorial_step_7_title
};
private final int[] stepContents = {
R.string.tutorial_step_1_content,
R.string.tutorial_step_2_content,
R.string.tutorial_step_3_content,
R.string.tutorial_step_4_content,
R.string.tutorial_step_5_content,
R.string.tutorial_step_6_content,
R.string.tutorial_step_7_content
};
public OnboardingTutorialDialog(Context context, LauncherActivity launcherActivity) {
super(context);
this.context = context;
this.launcherActivity = launcherActivity;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.dialog_onboarding_tutorial);
// Make background transparent for rounded corners
if (getWindow() != null) {
getWindow().setBackgroundDrawableResource(android.R.color.transparent);
}
// Initialize views
titleView = findViewById(R.id.tv_tutorial_title);
stepIndicatorView = findViewById(R.id.tv_step_indicator);
contentView = findViewById(R.id.tv_tutorial_content);
skipButton = findViewById(R.id.btn_skip);
previousButton = findViewById(R.id.btn_previous);
nextButton = findViewById(R.id.btn_next);
// Set click listeners
skipButton.setOnClickListener(this);
previousButton.setOnClickListener(this);
nextButton.setOnClickListener(this);
// Update content for first step
updateStepContent();
// Prevent dismissing by tapping outside
setCancelable(false);
}
@Override
public void onClick(View view) {
int id = view.getId();
if (id == R.id.btn_skip) {
finishTutorial();
} else if (id == R.id.btn_previous) {
if (currentStep > 1) {
currentStep--;
updateStepContent();
}
} else if (id == R.id.btn_next) {
if (currentStep < TOTAL_STEPS) {
currentStep++;
updateStepContent();
} else {
finishTutorial();
}
}
}
private void updateStepContent() {
// Update title
titleView.setText(stepTitles[currentStep - 1]);
// Update step indicator
stepIndicatorView.setText(currentStep + " / " + TOTAL_STEPS);
// Update content
contentView.setText(stepContents[currentStep - 1]);
// Update button visibility and text
if (currentStep == 1) {
previousButton.setVisibility(View.GONE);
} else {
previousButton.setVisibility(View.VISIBLE);
}
if (currentStep == TOTAL_STEPS) {
nextButton.setText(R.string.onboarding_finish);
skipButton.setVisibility(View.GONE);
} else {
nextButton.setText(R.string.onboarding_next);
skipButton.setVisibility(View.VISIBLE);
}
}
private void finishTutorial() {
// Mark tutorial as completed
DbUtils.setTutorialCompleted(true);
DbUtils.setFirstTimeUser(false);
// Dismiss dialog
dismiss();
}
}
@@ -63,6 +63,8 @@ public class DbUtils {
private static final String APPS_SORTS_TYPE = "apps_sorts_types"; private static final String APPS_SORTS_TYPE = "apps_sorts_types";
private static final String DOUBLE_TAP_TO_LOCK = "double_tap_to_lock"; private static final String DOUBLE_TAP_TO_LOCK = "double_tap_to_lock";
private static final String SEARCH_BAR_GESTURE = "search_bar_gesture"; private static final String SEARCH_BAR_GESTURE = "search_bar_gesture";
private static final String FIRST_TIME_USER = "first_time_user";
private static final String TUTORIAL_COMPLETED = "tutorial_completed";
public static void init(Context context) { public static void init(Context context) {
@@ -395,6 +397,22 @@ public class DbUtils {
SpUtils.getInstance().putBoolean(DOUBLE_TAP_TO_LOCK, enabled); SpUtils.getInstance().putBoolean(DOUBLE_TAP_TO_LOCK, enabled);
} }
public static boolean isFirstTimeUser() {
return SpUtils.getInstance().getBoolean(FIRST_TIME_USER, true);
}
public static void setFirstTimeUser(boolean isFirstTime) {
SpUtils.getInstance().putBoolean(FIRST_TIME_USER, isFirstTime);
}
public static boolean isTutorialCompleted() {
return SpUtils.getInstance().getBoolean(TUTORIAL_COMPLETED, false);
}
public static void setTutorialCompleted(boolean completed) {
SpUtils.getInstance().putBoolean(TUTORIAL_COMPLETED, completed);
}
/** /**
* Check if search bar gesture is enabled * Check if search bar gesture is enabled
*/ */
@@ -295,6 +295,23 @@
android:background="#DEDEDE" android:background="#DEDEDE"
android:foregroundGravity="center_vertical|center_horizontal" /> android:foregroundGravity="center_vertical|center_horizontal" />
<TextView
android:id="@+id/settings_tutorial"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:text="@string/tutorial_replay"
android:textSize="20sp" />
<View
android:layout_width="170dp"
android:layout_height="1dp"
android:layout_gravity="center|center_horizontal|center_vertical"
android:background="#DEDEDE"
android:foregroundGravity="center_vertical|center_horizontal" />
<TextView <TextView
android:id="@+id/settings_restart_launcher" android:id="@+id/settings_restart_launcher"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:background="@drawable/dialog_background"
android:orientation="vertical"
android:padding="24dp">
<!-- Title -->
<TextView
android:id="@+id/tv_tutorial_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="start"
android:text="@string/onboarding_title"
android:textColor="@android:color/white"
android:textSize="24sp"
android:textStyle="bold" />
<!-- Step indicator -->
<TextView
android:id="@+id/tv_step_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center"
android:text="1 / 7"
android:textColor="@android:color/white"
android:textSize="14sp"
android:alpha="0.7" />
<!-- Content -->
<TextView
android:id="@+id/tv_tutorial_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:lineSpacingExtra="4dp"
android:text="@string/tutorial_step_1_content"
android:textColor="@android:color/white"
android:textSize="16sp" />
<!-- Buttons container -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<!-- Skip button -->
<TextView
android:id="@+id/btn_skip"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:padding="12dp"
android:text="@string/onboarding_skip"
android:textColor="@android:color/white"
android:textSize="14sp" />
<!-- Previous button -->
<TextView
android:id="@+id/btn_previous"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:padding="12dp"
android:text="@string/onboarding_previous"
android:textColor="@android:color/white"
android:textSize="14sp"
android:visibility="gone" />
<!-- Next/Finish button -->
<TextView
android:id="@+id/btn_next"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:padding="12dp"
android:text="@string/onboarding_next"
android:textColor="@android:color/white"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
+32
View File
@@ -107,6 +107,38 @@
<string name="double_tap_to_lock_enabled">Double Tap Lock:On</string> <string name="double_tap_to_lock_enabled">Double Tap Lock:On</string>
<string name="double_tap_to_lock_disabled">Double Tap Lock:Off</string> <string name="double_tap_to_lock_disabled">Double Tap Lock:Off</string>
<!-- Onboarding Tutorial -->
<string name="onboarding_title">Welcome to Lunox</string>
<string name="onboarding_skip">Skip</string>
<string name="onboarding_next">Next</string>
<string name="onboarding_previous">Previous</string>
<string name="onboarding_finish">Finish</string>
<string name="onboarding_close">Close</string>
<!-- Tutorial Steps -->
<string name="tutorial_step_1_title">Welcome to Lunox Launcher</string>
<string name="tutorial_step_1_content">Lunox is a minimalist text-based launcher that puts simplicity first. All your apps are displayed as text in a clean, distraction-free interface.</string>
<string name="tutorial_step_2_title">Opening Apps</string>
<string name="tutorial_step_2_content">Simply tap on any app name to open it. Apps are sorted alphabetically by default, but you can change the sorting in settings.</string>
<string name="tutorial_step_3_title">Search Functionality</string>
<string name="tutorial_step_3_content">Swipe down to up anywhere on the screen to open the search bar. Start typing to quickly find and launch any app.</string>
<string name="tutorial_step_4_title">App Options</string>
<string name="tutorial_step_4_content">Long press on any app to access options like rename, hide, uninstall, change colors, and more customization features.</string>
<string name="tutorial_step_5_title">Launcher Settings</string>
<string name="tutorial_step_5_content">Long press on empty space to access launcher settings. Here you can change themes, fonts, sorting options, and other preferences.</string>
<string name="tutorial_step_6_title">Customization</string>
<string name="tutorial_step_6_content">Personalize your launcher with different themes, custom colors for apps, font options, and various sorting methods to make it truly yours.</string>
<string name="tutorial_step_7_title">You\'re All Set!</string>
<string name="tutorial_step_7_content">That\'s all you need to know to get started with Lunox. Enjoy your clean, minimalist launcher experience!</string>
<string name="tutorial_replay">Show Tutorial</string>
<!-- Accessibility service description --> <!-- Accessibility service description -->
<string name="accessibility_service_description">Lunox Accessibility Service for lock screen functionality. This service allows double tap to lock the screen.</string> <string name="accessibility_service_description">Lunox Accessibility Service for lock screen functionality. This service allows double tap to lock the screen.</string>
<string name="set_as_default_launcher">Set as default launcher</string> <string name="set_as_default_launcher">Set as default launcher</string>
@@ -14,6 +14,8 @@
- Fixed Duplicate Dialogs on Long Press - Fixed Duplicate Dialogs on Long Press
- Enhanced gesture detection for better responsiveness - Enhanced gesture detection for better responsiveness
- Fixed WhiteOnGrey and BlackOnGrey themes using distinct colors for better visibility - Fixed WhiteOnGrey and BlackOnGrey themes using distinct colors for better visibility
- Added Onboarding Tutorial for new users with step-by-step walkthrough
- Tutorial can be replayed from launcher settings
* Technical Improvements * Technical Improvements
@@ -23,6 +25,8 @@
- Improved dialog management to prevent duplicate dialogs - Improved dialog management to prevent duplicate dialogs
- Better empty area detection for long press functionality - Better empty area detection for long press functionality
- Enhanced search functionality with smooth animations - Enhanced search functionality with smooth animations
- Added comprehensive onboarding system with multi-step tutorial dialog
- Implemented first-time user detection and tutorial completion tracking
* Project Source Code Changes * Project Source Code Changes