mirror of
https://github.com/anonfaded/FadCam.git
synced 2026-03-13 09:00:17 +08:00
feat: enhance onboarding experience by adding human verification slide; implement OnboardingHumanFragment with interactive checkboxes, integrate PalestineRainView for animations, and update onboarding layouts and resources
This commit is contained in:
@@ -6,6 +6,7 @@ public abstract class Constants {
|
||||
|
||||
public static final String PREFS_NAME = "app_prefs";
|
||||
public static final String LANGUAGE_KEY = "language";
|
||||
public static final String COMPLETED_ONBOARDING_KEY = "pref_completed_onboarding";
|
||||
|
||||
public static final String PREF_VIDEO_RESOLUTION_WIDTH = "video_resolution_width";
|
||||
public static final String PREF_VIDEO_RESOLUTION_HEIGHT = "video_resolution_height";
|
||||
|
||||
@@ -358,24 +358,19 @@ public class SharedPreferencesManager {
|
||||
}
|
||||
// ----- Fix Ended for this class (SharedPreferencesManager_audio_input_source) -----
|
||||
|
||||
// ----- Fix Start: Add onboarding preference methods -----
|
||||
private static final String PREF_SHOW_ONBOARDING = "PREF_SHOW_ONBOARDING";
|
||||
private static final boolean DEFAULT_SHOW_ONBOARDING = true;
|
||||
|
||||
/**
|
||||
* Returns true if onboarding should be shown (default: true).
|
||||
*/
|
||||
// ----- Fix Start for method(onboarding) -----
|
||||
// Using the proper constant from Constants class
|
||||
public boolean isShowOnboarding() {
|
||||
return sharedPreferences.getBoolean(PREF_SHOW_ONBOARDING, DEFAULT_SHOW_ONBOARDING);
|
||||
// Return true if onboarding hasn't been completed
|
||||
return !sharedPreferences.getBoolean(Constants.COMPLETED_ONBOARDING_KEY, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether onboarding should be shown.
|
||||
* @param show true to show onboarding, false to skip
|
||||
*/
|
||||
public void setShowOnboarding(boolean show) {
|
||||
sharedPreferences.edit().putBoolean(PREF_SHOW_ONBOARDING, show).apply();
|
||||
// Store the opposite value in COMPLETED_ONBOARDING_KEY
|
||||
// If show is true, it means onboarding is not completed
|
||||
// If show is false, it means onboarding is completed
|
||||
sharedPreferences.edit().putBoolean(Constants.COMPLETED_ONBOARDING_KEY, !show).apply();
|
||||
}
|
||||
// ----- Fix End: Add onboarding preference methods -----
|
||||
// ----- Fix End for method(onboarding) -----
|
||||
|
||||
}
|
||||
@@ -1490,11 +1490,11 @@ public class HomeFragment extends Fragment {
|
||||
|
||||
private void setupButtonListeners() {
|
||||
buttonStartStop.setOnClickListener(v -> {
|
||||
if (recordingState.equals(RecordingState.NONE)) {
|
||||
startRecording();
|
||||
} else {
|
||||
stopRecording();
|
||||
updateStats();
|
||||
if (recordingState.equals(RecordingState.NONE)) {
|
||||
startRecording();
|
||||
} else {
|
||||
stopRecording();
|
||||
updateStats();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.widget.TextView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
import com.fadcam.MainActivity;
|
||||
import com.fadcam.SharedPreferencesManager;
|
||||
import com.github.appintro.AppIntro;
|
||||
import com.github.appintro.AppIntroFragment;
|
||||
@@ -26,6 +27,8 @@ import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -34,12 +37,85 @@ import java.util.List;
|
||||
* OnboardingActivity shows the app intro slides using AppIntro.
|
||||
*/
|
||||
public class OnboardingActivity extends AppIntro {
|
||||
private boolean isLastSlide = false;
|
||||
private boolean isFirstSlide = true; // Track if we're on the first slide
|
||||
// Use View instead of Button to avoid class cast exceptions
|
||||
private View backButton;
|
||||
private View nextButton;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
addSlide(AppIntroCustomLayoutFragment.newInstance(R.layout.onboarding_intro_slide));
|
||||
addSlide(AppIntroCustomLayoutFragment.newInstance(R.layout.onboarding_language_slide));
|
||||
addSlide(new OnboardingPermissionsFragment());
|
||||
addSlide(new OnboardingHumanFragment());
|
||||
|
||||
// Hide only the Done button while keeping navigation buttons
|
||||
setSkipButtonEnabled(false); // No skip button needed
|
||||
setNextPageSwipeLock(false);
|
||||
setIndicatorEnabled(true);
|
||||
|
||||
// Use wizard mode to replace Skip with Back button
|
||||
setWizardMode(true);
|
||||
|
||||
// Fix for indicator dots - use custom colors
|
||||
setIndicatorColor(
|
||||
ContextCompat.getColor(this, R.color.white), // Selected dot
|
||||
ContextCompat.getColor(this, R.color.gray500) // Unselected dot
|
||||
);
|
||||
|
||||
// Disable color transitions as our slides don't implement SlideBackgroundColorHolder
|
||||
setColorTransitionsEnabled(false);
|
||||
|
||||
// Set fade transition effect between slides
|
||||
ViewPager viewPager = findViewById(com.github.appintro.R.id.view_pager);
|
||||
if (viewPager != null) {
|
||||
viewPager.setPageTransformer(true, new FadePageTransformer());
|
||||
|
||||
// Listen for page changes to update navigation buttons
|
||||
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
updateNavigationButtons(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize navigation button references
|
||||
backButton = findViewById(com.github.appintro.R.id.back);
|
||||
nextButton = findViewById(com.github.appintro.R.id.next);
|
||||
|
||||
// Completely remove the Done button
|
||||
View doneButton = findViewById(com.github.appintro.R.id.done);
|
||||
if (doneButton != null) {
|
||||
// First make it invisible
|
||||
doneButton.setVisibility(View.GONE);
|
||||
|
||||
// Zero out its dimensions
|
||||
ViewGroup.LayoutParams params = doneButton.getLayoutParams();
|
||||
if (params != null) {
|
||||
params.width = 0;
|
||||
params.height = 0;
|
||||
doneButton.setLayoutParams(params);
|
||||
}
|
||||
|
||||
// Also try to remove it from its parent
|
||||
ViewGroup parent = (ViewGroup) doneButton.getParent();
|
||||
if (parent != null) {
|
||||
parent.removeView(doneButton);
|
||||
}
|
||||
}
|
||||
|
||||
// Update navigation buttons initially
|
||||
updateNavigationButtons(0);
|
||||
|
||||
getSupportFragmentManager().registerFragmentLifecycleCallbacks(new androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks() {
|
||||
@Override
|
||||
public void onFragmentViewCreated(@NonNull androidx.fragment.app.FragmentManager fm, @NonNull Fragment f, @Nullable View v, @Nullable Bundle savedInstanceState) {
|
||||
@@ -168,6 +244,48 @@ public class OnboardingActivity extends AppIntro {
|
||||
}, true);
|
||||
}
|
||||
|
||||
// Method to update navigation buttons based on current position
|
||||
private void updateNavigationButtons(int position) {
|
||||
ViewPager viewPager = findViewById(com.github.appintro.R.id.view_pager);
|
||||
if (viewPager == null || viewPager.getAdapter() == null) return;
|
||||
|
||||
int slideCount = viewPager.getAdapter().getCount();
|
||||
isFirstSlide = position == 0;
|
||||
isLastSlide = position == slideCount - 1;
|
||||
|
||||
if (backButton != null) {
|
||||
backButton.setVisibility(isFirstSlide ? View.INVISIBLE : View.VISIBLE);
|
||||
}
|
||||
|
||||
if (nextButton != null) {
|
||||
nextButton.setVisibility(isLastSlide ? View.INVISIBLE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
// Implement a custom page transformer for fade transitions
|
||||
private static class FadePageTransformer implements ViewPager.PageTransformer {
|
||||
private static final float MIN_ALPHA = 0.0f;
|
||||
private static final float MAX_ALPHA = 1.0f;
|
||||
|
||||
@Override
|
||||
public void transformPage(@NonNull View page, float position) {
|
||||
if (position < -1) { // Page is way off-screen to the left
|
||||
page.setAlpha(MIN_ALPHA);
|
||||
} else if (position <= 1) { // Page is visible or entering
|
||||
// Calculate alpha based on position
|
||||
float alphaFactor = Math.max(MIN_ALPHA, 1 - Math.abs(position));
|
||||
page.setAlpha(alphaFactor);
|
||||
|
||||
// Apply a slight scale effect
|
||||
float scaleFactor = Math.max(0.85f, 1 - Math.abs(position * 0.15f));
|
||||
page.setScaleX(scaleFactor);
|
||||
page.setScaleY(scaleFactor);
|
||||
} else { // Page is way off-screen to the right
|
||||
page.setAlpha(MIN_ALPHA);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the language choose button for onboarding, using a Material dialog for selection.
|
||||
* @param chooseButton The MaterialButton to setup.
|
||||
@@ -243,18 +361,24 @@ public class OnboardingActivity extends AppIntro {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is still overridden to handle any case where the done action might be triggered,
|
||||
* even though we've hidden the done button and are using our custom Start button.
|
||||
*/
|
||||
@Override
|
||||
public void onDonePressed(@Nullable androidx.fragment.app.Fragment currentFragment) {
|
||||
// Mark onboarding as completed
|
||||
SharedPreferencesManager.getInstance(this)
|
||||
.sharedPreferences.edit().putBoolean("PREF_SHOW_ONBOARDING", false).apply();
|
||||
|
||||
// Launch MainActivity
|
||||
Intent intent = new Intent(this, com.fadcam.MainActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
|
||||
protected void onDonePressed(Fragment currentFragment) {
|
||||
super.onDonePressed(currentFragment);
|
||||
finishOnboarding();
|
||||
}
|
||||
|
||||
public void finishOnboarding() {
|
||||
// Mark onboarding as completed
|
||||
SharedPreferencesManager sharedPreferencesManager = SharedPreferencesManager.getInstance(this);
|
||||
sharedPreferencesManager.sharedPreferences.edit().putBoolean(com.fadcam.Constants.COMPLETED_ONBOARDING_KEY, true).apply();
|
||||
|
||||
// Return to MainActivity or just finish
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
109
app/src/main/java/com/fadcam/ui/OnboardingHumanFragment.java
Normal file
109
app/src/main/java/com/fadcam/ui/OnboardingHumanFragment.java
Normal file
@@ -0,0 +1,109 @@
|
||||
package com.fadcam.ui;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.graphics.PorterDuff;
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
import com.fadcam.R;
|
||||
|
||||
public class OnboardingHumanFragment extends Fragment {
|
||||
private boolean checked1 = false;
|
||||
private boolean checked2 = false;
|
||||
private MaterialButton continueButton;
|
||||
private ImageView icon1, icon2;
|
||||
private TextView label1, label2;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View v = inflater.inflate(R.layout.onboarding_human_slide, container, false);
|
||||
|
||||
LinearLayout row1 = v.findViewById(R.id.checkbox_row_1);
|
||||
LinearLayout row2 = v.findViewById(R.id.checkbox_row_2);
|
||||
icon1 = v.findViewById(R.id.checkbox_icon_1);
|
||||
icon2 = v.findViewById(R.id.checkbox_icon_2);
|
||||
label1 = v.findViewById(R.id.checkbox_label_1);
|
||||
label2 = v.findViewById(R.id.checkbox_label_2);
|
||||
continueButton = v.findViewById(R.id.btn_human_continue);
|
||||
|
||||
// Find the PalestineRainView and prevent it from intercepting touch events
|
||||
View palestineRainView = v.findViewById(R.id.palestineRainView);
|
||||
if (palestineRainView != null) {
|
||||
palestineRainView.setClickable(false);
|
||||
palestineRainView.setFocusable(false);
|
||||
}
|
||||
|
||||
// Configure the LottieAnimationView
|
||||
LottieAnimationView lottieHuman = v.findViewById(R.id.lottieHuman);
|
||||
if (lottieHuman != null) {
|
||||
lottieHuman.setSpeed(0.8f);
|
||||
}
|
||||
|
||||
// Set initial button state
|
||||
continueButton.setEnabled(false);
|
||||
continueButton.setAlpha(0.6f);
|
||||
|
||||
// Get the redPastel color
|
||||
int redPastelColor = ContextCompat.getColor(requireContext(), R.color.redPastel);
|
||||
|
||||
View.OnClickListener update = view -> {
|
||||
continueButton.setEnabled(checked1 && checked2);
|
||||
continueButton.setAlpha((checked1 && checked2) ? 1f : 0.6f);
|
||||
};
|
||||
|
||||
View.OnClickListener toggle1 = view -> {
|
||||
checked1 = !checked1;
|
||||
// Set the appropriate checkbox image and apply redPastel tint when checked
|
||||
icon1.setImageResource(checked1 ? R.drawable.placeholder_checkbox_checked : R.drawable.placeholder_checkbox_outline);
|
||||
if (checked1) {
|
||||
// Apply redPastel tint to the checked checkbox
|
||||
icon1.setColorFilter(redPastelColor, PorterDuff.Mode.SRC_IN);
|
||||
} else {
|
||||
// Clear any color filter for unchecked state
|
||||
icon1.clearColorFilter();
|
||||
}
|
||||
update.onClick(view);
|
||||
};
|
||||
|
||||
View.OnClickListener toggle2 = view -> {
|
||||
checked2 = !checked2;
|
||||
// Set the appropriate checkbox image and apply redPastel tint when checked
|
||||
icon2.setImageResource(checked2 ? R.drawable.placeholder_checkbox_checked : R.drawable.placeholder_checkbox_outline);
|
||||
if (checked2) {
|
||||
// Apply redPastel tint to the checked checkbox
|
||||
icon2.setColorFilter(redPastelColor, PorterDuff.Mode.SRC_IN);
|
||||
} else {
|
||||
// Clear any color filter for unchecked state
|
||||
icon2.clearColorFilter();
|
||||
}
|
||||
update.onClick(view);
|
||||
};
|
||||
|
||||
row1.setOnClickListener(toggle1);
|
||||
icon1.setOnClickListener(toggle1);
|
||||
label1.setOnClickListener(toggle1);
|
||||
row2.setOnClickListener(toggle2);
|
||||
icon2.setOnClickListener(toggle2);
|
||||
label2.setOnClickListener(toggle2);
|
||||
|
||||
continueButton.setOnClickListener(view -> {
|
||||
if (getActivity() instanceof OnboardingActivity) {
|
||||
((OnboardingActivity) getActivity()).finishOnboarding();
|
||||
} else {
|
||||
requireActivity().finish();
|
||||
}
|
||||
});
|
||||
|
||||
return v;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -34,41 +35,42 @@ public class OnboardingPermissionsFragment extends Fragment implements SlidePoli
|
||||
private MaterialButton grantButton;
|
||||
private int permissionRequestCount = 0; // Track how many times requested
|
||||
private boolean permanentlyDenied = false;
|
||||
private TextView permissionStatusText; // Text view to show status instead of toasts
|
||||
private boolean statusToastShown = false; // Flag to prevent multiple toasts
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.onboarding_permissions_slide, container, false);
|
||||
grantButton = view.findViewById(R.id.permissions_grant_button);
|
||||
permissionStatusText = view.findViewById(R.id.permission_status_text);
|
||||
if (permissionStatusText != null) {
|
||||
permissionStatusText.setVisibility(View.GONE); // Hide initially
|
||||
}
|
||||
grantButton.setEnabled(true);
|
||||
grantButton.setAlpha(1f);
|
||||
grantButton.setOnClickListener(v -> {
|
||||
if (permissionsGranted) {
|
||||
// Do nothing, or optionally show a toast
|
||||
Toast.makeText(requireContext(), R.string.permissions_granted, Toast.LENGTH_SHORT).show();
|
||||
showPermissionStatus(R.string.permissions_granted, true);
|
||||
} else {
|
||||
// Always show the runtime permission dialog, even if permanently denied
|
||||
requestAllPermissionsAlways();
|
||||
}
|
||||
});
|
||||
checkPermissionsAndUpdateUI();
|
||||
// ----- Fix Start: Always show runtime dialog if needed on first load -----
|
||||
if (!permissionsGranted && !permanentlyDenied) {
|
||||
requestAllPermissionsAlways();
|
||||
}
|
||||
// ----- Fix End: Always show runtime dialog if needed on first load -----
|
||||
|
||||
// ----- Fix Start: Add Open Settings link logic -----
|
||||
View openSettingsLink = view.findViewById(R.id.open_settings_link);
|
||||
if (openSettingsLink != null) {
|
||||
openSettingsLink.setOnClickListener(v2 -> {
|
||||
showManualPermissionDialog();
|
||||
Toast.makeText(requireContext(), R.string.open_settings, Toast.LENGTH_SHORT).show();
|
||||
showPermissionStatus(R.string.open_settings, false);
|
||||
});
|
||||
}
|
||||
// ----- Fix End: Add Open Settings link logic -----
|
||||
|
||||
// ----- Fix Start: Add Disable Battery Optimization button logic -----
|
||||
MaterialButton batteryOptButton = view.findViewById(R.id.disable_battery_optimization_button);
|
||||
if (batteryOptButton != null) {
|
||||
android.os.PowerManager pm = (android.os.PowerManager) requireContext().getSystemService(android.content.Context.POWER_SERVICE);
|
||||
@@ -84,10 +86,21 @@ public class OnboardingPermissionsFragment extends Fragment implements SlidePoli
|
||||
startActivity(intent);
|
||||
});
|
||||
}
|
||||
// ----- Fix End: Add Disable Battery Optimization button logic -----
|
||||
return view;
|
||||
}
|
||||
|
||||
private void showPermissionStatus(int stringResId, boolean success) {
|
||||
if (permissionStatusText != null) {
|
||||
permissionStatusText.setText(stringResId);
|
||||
permissionStatusText.setTextColor(ContextCompat.getColor(requireContext(),
|
||||
success ? R.color.green : R.color.redPastel));
|
||||
permissionStatusText.setVisibility(View.VISIBLE);
|
||||
} else if (!statusToastShown) {
|
||||
Toast.makeText(requireContext(), stringResId, Toast.LENGTH_SHORT).show();
|
||||
statusToastShown = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void requestAllPermissionsAlways() {
|
||||
// Always request, system will ignore already granted
|
||||
List<String> permissions = new ArrayList<>();
|
||||
@@ -155,20 +168,17 @@ public class OnboardingPermissionsFragment extends Fragment implements SlidePoli
|
||||
grantButton.setEnabled(false);
|
||||
grantButton.setAlpha(0.5f);
|
||||
grantButton.setText(R.string.permissions_granted);
|
||||
showPermissionStatus(R.string.permissions_granted, true);
|
||||
} else {
|
||||
grantButton.setEnabled(true);
|
||||
grantButton.setAlpha(1f);
|
||||
grantButton.setText(R.string.grant_permissions);
|
||||
}
|
||||
if (permissionsGranted) {
|
||||
Toast.makeText(requireContext(), R.string.permissions_granted, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
// Always re-check permission state when returning to this fragment
|
||||
checkPermissionsAndUpdateUI();
|
||||
}
|
||||
|
||||
@@ -188,9 +198,7 @@ public class OnboardingPermissionsFragment extends Fragment implements SlidePoli
|
||||
|
||||
@Override
|
||||
public void onUserIllegallyRequestedNextPage() {
|
||||
// Show toast and block navigation if permissions not granted
|
||||
Toast.makeText(requireContext(), R.string.permissions_note, Toast.LENGTH_SHORT).show();
|
||||
// Optionally, trigger permission dialog again
|
||||
showPermissionStatus(R.string.permissions_note, false);
|
||||
if (grantButton != null) {
|
||||
grantButton.performClick();
|
||||
}
|
||||
|
||||
175
app/src/main/java/com/fadcam/ui/PalestineRainView.java
Normal file
175
app/src/main/java/com/fadcam/ui/PalestineRainView.java
Normal file
@@ -0,0 +1,175 @@
|
||||
package com.fadcam.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Shader;
|
||||
import android.os.Handler;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.fadcam.R;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* PalestineRainView: Animated falling icons for human verification slide.
|
||||
* Uses palestine.png as the icon.
|
||||
*/
|
||||
public class PalestineRainView extends View {
|
||||
private static class IconDrop {
|
||||
float y, speed, offset;
|
||||
float tilt;
|
||||
}
|
||||
|
||||
private IconDrop[] drops;
|
||||
private int numColumns;
|
||||
private int iconSizePx;
|
||||
private int gapPx;
|
||||
private int viewWidth, viewHeight;
|
||||
private Bitmap iconBitmap;
|
||||
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Handler handler = new Handler();
|
||||
private static final int FRAME_DELAY = 24; // ms - slightly slower animation
|
||||
private static final float SPEED_MIN = 2.0f; // slower minimum speed
|
||||
private static final float SPEED_MAX = 4.5f; // slower maximum speed
|
||||
private final Random random = new Random();
|
||||
|
||||
public PalestineRainView(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
public PalestineRainView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
public PalestineRainView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
// Set this view to not intercept touch events
|
||||
setClickable(false);
|
||||
setFocusable(false);
|
||||
|
||||
iconSizePx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28, context.getResources().getDisplayMetrics());
|
||||
gapPx = 0; // denser, no gap
|
||||
iconBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.palestine_main);
|
||||
paint.setAlpha(255); // Full opacity
|
||||
startAnimation();
|
||||
}
|
||||
|
||||
private void startAnimation() {
|
||||
handler.post(frameRunnable);
|
||||
}
|
||||
|
||||
private final Runnable frameRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateDrops();
|
||||
invalidate();
|
||||
handler.postDelayed(this, FRAME_DELAY);
|
||||
}
|
||||
};
|
||||
|
||||
private void updateDrops() {
|
||||
if (drops == null) return;
|
||||
for (int i = 0; i < numColumns; i++) {
|
||||
IconDrop drop = drops[i];
|
||||
drop.y += drop.speed;
|
||||
if (drop.y > viewHeight) {
|
||||
drop.y = -iconSizePx - drop.offset;
|
||||
drop.speed = SPEED_MIN + random.nextFloat() * 0.7f;
|
||||
drop.offset = random.nextInt(iconSizePx * 2);
|
||||
if (random.nextBoolean()) {
|
||||
drop.tilt = -32f + random.nextFloat() * 20f;
|
||||
} else {
|
||||
drop.tilt = 12f + random.nextFloat() * 20f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
if (iconBitmap == null || drops == null) return;
|
||||
Paint iconPaint = new Paint(paint);
|
||||
android.graphics.LinearGradient iconGradient = new android.graphics.LinearGradient(
|
||||
0, 0, 0, iconSizePx,
|
||||
0xFFFFFFFF, 0x00FFFFFF, android.graphics.Shader.TileMode.CLAMP
|
||||
);
|
||||
iconPaint.setShader(iconGradient);
|
||||
if (numColumns == 1) {
|
||||
float x = (viewWidth - iconSizePx) / 2f;
|
||||
IconDrop drop = drops[0];
|
||||
Rect dest = new Rect((int)x, (int)drop.y, (int)x + iconSizePx, (int)drop.y + iconSizePx);
|
||||
canvas.save();
|
||||
canvas.rotate(drop.tilt, x + iconSizePx / 2f, drop.y + iconSizePx / 2f);
|
||||
canvas.drawBitmap(iconBitmap, null, dest, iconPaint);
|
||||
canvas.restore();
|
||||
} else {
|
||||
for (int i = 0; i < numColumns; i++) {
|
||||
float x = i * (iconSizePx + gapPx);
|
||||
IconDrop drop = drops[i];
|
||||
float y = drop.y;
|
||||
Rect dest = new Rect((int)x, (int)y, (int)x + iconSizePx, (int)y + iconSizePx);
|
||||
canvas.save();
|
||||
canvas.rotate(drop.tilt, x + iconSizePx / 2f, y + iconSizePx / 2f);
|
||||
canvas.drawBitmap(iconBitmap, null, dest, iconPaint);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
int gradientHeight = (int) (viewHeight * 0.4f);
|
||||
Paint gradPaint = new Paint();
|
||||
LinearGradient grad = new LinearGradient(
|
||||
0, viewHeight,
|
||||
0, viewHeight - gradientHeight,
|
||||
0xCC000000, 0x00000000,
|
||||
Shader.TileMode.CLAMP
|
||||
);
|
||||
gradPaint.setShader(grad);
|
||||
canvas.drawRect(0, viewHeight - gradientHeight, viewWidth, viewHeight, gradPaint);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
viewWidth = w;
|
||||
viewHeight = h;
|
||||
numColumns = Math.max(2, w / (iconSizePx + gapPx) * 2);
|
||||
drops = new IconDrop[numColumns];
|
||||
for (int i = 0; i < numColumns; i++) {
|
||||
drops[i] = new IconDrop();
|
||||
drops[i].y = -random.nextInt(h + iconSizePx);
|
||||
drops[i].speed = SPEED_MIN + random.nextFloat() * 0.7f;
|
||||
drops[i].offset = random.nextInt(iconSizePx * 2);
|
||||
if (random.nextBoolean()) {
|
||||
drops[i].tilt = -32f + random.nextFloat() * 20f;
|
||||
} else {
|
||||
drops[i].tilt = 12f + random.nextFloat() * 20f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
// Don't intercept touch events, pass them to parent
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1643,12 +1643,12 @@ public class SettingsFragment extends Fragment {
|
||||
String newLangCode = getLanguageCode(which);
|
||||
if (!newLangCode.equals(sharedPreferencesManager.getLanguage())) {
|
||||
saveLanguagePreference(newLangCode);
|
||||
if(getActivity() instanceof MainActivity){
|
||||
if(getActivity() instanceof MainActivity){
|
||||
((MainActivity) requireActivity()).applyLanguage(newLangCode);
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Language changed. Restart app to apply.", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(getContext(), "Language changed. Restart app to apply.", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
chooseButton.setText(languages[which]);
|
||||
dialog.dismiss();
|
||||
})
|
||||
|
||||
150
app/src/main/res/layout/onboarding_human_slide.xml
Normal file
150
app/src/main/res/layout/onboarding_human_slide.xml
Normal file
@@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/amoled_surface"
|
||||
android:fillViewport="true">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.fadcam.ui.PalestineRainView
|
||||
android:id="@+id/palestineRainView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0.35" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="120dp"
|
||||
android:paddingTop="@dimen/onboarding_nav_margin"
|
||||
android:paddingBottom="@dimen/onboarding_nav_margin">
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/lottieHuman"
|
||||
android:layout_width="180dp"
|
||||
android:layout_height="120dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:lottie_fileName="document_rules_green_checkmark.json"
|
||||
app:lottie_autoPlay="true"
|
||||
app:lottie_loop="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvHumanTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/onboarding_human_title"
|
||||
android:textSize="@dimen/onboarding_title_size"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="monospace"
|
||||
android:textColor="@color/redPastel"
|
||||
android:gravity="center"
|
||||
android:layout_marginLeft="@dimen/onboarding_margin"
|
||||
android:layout_marginRight="@dimen/onboarding_margin"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvHumanDesc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/onboarding_human_desc"
|
||||
android:textSize="@dimen/onboarding_desc_size"
|
||||
android:fontFamily="monospace"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="@dimen/onboarding_margin"
|
||||
android:layout_marginLeft="@dimen/onboarding_margin"
|
||||
android:layout_marginRight="@dimen/onboarding_margin"/>
|
||||
|
||||
<!-- Custom Checkbox Row 1 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/checkbox_row_1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginTop="@dimen/onboarding_margin"
|
||||
android:layout_marginLeft="@dimen/onboarding_margin"
|
||||
android:layout_marginRight="@dimen/onboarding_margin"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/checkbox_icon_1"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/placeholder_checkbox_outline"
|
||||
android:contentDescription="@string/onboarding_human_checkbox1"
|
||||
android:layout_marginRight="12dp"
|
||||
android:padding="2dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/checkbox_label_1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/onboarding_human_checkbox1"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="monospace"
|
||||
android:textSize="@dimen/onboarding_desc_size"/>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Custom Checkbox Row 2 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/checkbox_row_2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginLeft="@dimen/onboarding_margin"
|
||||
android:layout_marginRight="@dimen/onboarding_margin"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/checkbox_icon_2"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/placeholder_checkbox_outline"
|
||||
android:contentDescription="@string/onboarding_human_checkbox2"
|
||||
android:layout_marginRight="12dp"
|
||||
android:padding="2dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/checkbox_label_2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/onboarding_human_checkbox2"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="monospace"
|
||||
android:textSize="@dimen/onboarding_desc_size"/>
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_human_continue"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/onboarding_human_button"
|
||||
android:fontFamily="monospace"
|
||||
android:textSize="@dimen/onboarding_button_text_size"
|
||||
android:textColor="@color/white"
|
||||
android:backgroundTint="@color/redPastel"
|
||||
app:cornerRadius="12dp"
|
||||
android:layout_marginTop="@dimen/onboarding_margin"
|
||||
android:layout_marginLeft="@dimen/onboarding_nav_margin"
|
||||
android:layout_marginRight="@dimen/onboarding_nav_margin"/>
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
</ScrollView>
|
||||
@@ -57,13 +57,30 @@
|
||||
android:layout_marginTop="@dimen/onboarding_margin"
|
||||
android:layout_marginBottom="@dimen/onboarding_margin" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/permission_status_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/permissions_granted"
|
||||
android:textSize="@dimen/onboarding_desc_size"
|
||||
android:fontFamily="monospace"
|
||||
android:textColor="@color/green"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
android:paddingLeft="@dimen/onboarding_margin"
|
||||
android:paddingRight="@dimen/onboarding_margin"
|
||||
app:layout_constraintTop_toBottomOf="@id/permissionsDescription"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/permissionsListContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
app:layout_constraintTop_toBottomOf="@id/permissionsDescription"
|
||||
app:layout_constraintTop_toBottomOf="@id/permission_status_text"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="@dimen/onboarding_margin"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="gray">#1f1e23</color>
|
||||
<color name="gray500">#9E9E9E</color>
|
||||
<color name="redPastel">#E43C3C</color>
|
||||
<color name="greenPastel">#77DD77</color>
|
||||
<color name="colorPrimary">#cfbafd</color>
|
||||
@@ -17,6 +18,7 @@
|
||||
<color name="danger_red">#D32F2F</color>
|
||||
<color name="danger_red_light">#F44336</color>
|
||||
<color name="colorError">#D32F2F</color>
|
||||
<color name="green">#4CAF50</color>
|
||||
|
||||
<!-- Added for Trash Item Remaining Time Display -->
|
||||
<color name="colorWarning">#FFA726</color> <!-- Orange A400 -->
|
||||
|
||||
@@ -453,4 +453,10 @@ Recommended: 192000 bps, 48000 Hz for most users.</string>
|
||||
|
||||
<string name="disable_battery_optimization">Disable Battery Optimization</string>
|
||||
|
||||
</resources>
|
||||
<string name="onboarding_human_title">Verify if you are a human</string>
|
||||
<string name="onboarding_human_desc">Hmmmm... alright. These rules are non-negotiable. If you can\'t agree, please uninstall FadCam right now.</string>
|
||||
<string name="onboarding_human_checkbox1">I won\'t use FadCam to break any laws, invade anyone\'s privacy, or be a creep. The dev\'s out of the picture if I mess up. (That\'s on me.)</string>
|
||||
<string name="onboarding_human_checkbox2">I already love FadCam before even using it. (Not optional)</string>
|
||||
<string name="onboarding_human_button">Let me in!</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -18,4 +18,5 @@ android.useAndroidX=true
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonTransitiveRClass=true
|
||||
org.gradle.configuration-cache=true
|
||||
Reference in New Issue
Block a user