diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
new file mode 100644
index 0000000..91a69d1
--- /dev/null
+++ b/.github/workflows/android.yml
@@ -0,0 +1,26 @@
+name: Android CI
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ branches: [ "master" ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+ cache: gradle
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+ - name: Build with Gradle
+ run: ./gradlew build
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..1e9ba41
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+Valv
\ No newline at end of file
diff --git a/.idea/markdown.xml b/.idea/markdown.xml
new file mode 100644
index 0000000..c61ea33
--- /dev/null
+++ b/.idea/markdown.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 12534e6..4b3bd38 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -14,7 +14,7 @@ configure {
minSdk = 28
targetSdk = 36
versionCode = 41
- versionName = "2.4.1"
+ versionName = "2.4.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -87,6 +87,7 @@ dependencies {
implementation(libs.glide)
implementation(libs.about.libraries)
implementation(libs.about.libraries.compose)
+ implementation("androidx.palette:palette:1.0.0")
}
aboutLibraries {
diff --git a/app/src/main/java/se/arctosoft/vault/DirectoryAllFragment.java b/app/src/main/java/se/arctosoft/vault/DirectoryAllFragment.java
index 2d95578..5a38e6a 100644
--- a/app/src/main/java/se/arctosoft/vault/DirectoryAllFragment.java
+++ b/app/src/main/java/se/arctosoft/vault/DirectoryAllFragment.java
@@ -5,6 +5,9 @@
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
import android.view.View;
import androidx.activity.OnBackPressedCallback;
@@ -12,6 +15,7 @@
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
+import androidx.recyclerview.widget.GridLayoutManager;
import java.util.ArrayList;
import java.util.List;
@@ -26,6 +30,14 @@ public class DirectoryAllFragment extends DirectoryBaseFragment {
private static final String TAG = "DirectoryAllFragment";
private int foundFiles = 0, foundFolders = 0;
+ private com.google.android.material.bottomnavigation.BottomNavigationView bottomNav;
+ private boolean isNavPillHidden = false; // The lock that prevents scroll lag!
+
+ // --- NEW: Variables for the Breathing Grid ---
+ private ScaleGestureDetector scaleGestureDetector;
+ private float scaleFactor = 1.0f;
+ private static final int MIN_COLUMNS = 2;
+ private static final int MAX_COLUMNS = 6;
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
@@ -76,6 +88,83 @@ public void handleOnBackPressed() {
findAllFiles();
}
+ // --- RESPONSIVE BOTTOM NAV LOGIC ---
+ bottomNav = binding.getRoot().findViewById(R.id.bottom_navigation);
+ if (bottomNav != null) {
+ bottomNav.setVisibility(View.VISIBLE);
+ bottomNav.setSelectedItemId(R.id.nav_all_files);
+
+ bottomNav.setOnItemSelectedListener(item -> {
+ int id = item.getItemId();
+ if (id == R.id.nav_albums) {
+ navController.popBackStack();
+ return true;
+ } else if (id == R.id.nav_all_files) {
+ return true;
+ }
+ return false;
+ });
+
+ // Smooth, Optimized Hide-on-Scroll Animation
+ binding.recyclerView.addOnScrollListener(new androidx.recyclerview.widget.RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(@NonNull androidx.recyclerview.widget.RecyclerView recyclerView, int dx, int dy) {
+ super.onScrolled(recyclerView, dx, dy);
+
+ // Don't animate if fullscreen media is open
+ if (galleryViewModel.isViewpagerVisible()) return;
+
+ // Ensure we only trigger the animation ONCE per state change to prevent lag
+ if (dy > 15 && !isNavPillHidden) {
+ isNavPillHidden = true; // Lock it
+ bottomNav.animate().translationY(bottomNav.getHeight() + 150).setDuration(200).start();
+ } else if (dy < -15 && isNavPillHidden) {
+ isNavPillHidden = false; // Unlock it
+ bottomNav.animate().translationY(0).setDuration(200).start();
+ }
+ }
+ });
+ }
+ // -----------------------------
+
+ // --- NEW: THE "BREATHING" GRID (PINCH TO ZOOM COLUMNS) ---
+ scaleGestureDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
+ @Override
+ public boolean onScale(@NonNull ScaleGestureDetector detector) {
+ scaleFactor *= detector.getScaleFactor();
+
+ if (binding.recyclerView.getLayoutManager() instanceof GridLayoutManager) {
+ GridLayoutManager layoutManager = (GridLayoutManager) binding.recyclerView.getLayoutManager();
+ int currentSpans = layoutManager.getSpanCount();
+
+ // Pinching Out (Zooming In) -> Fewer Columns
+ if (scaleFactor > 1.25f && currentSpans > MIN_COLUMNS) {
+ layoutManager.setSpanCount(currentSpans - 1);
+ scaleFactor = 1.0f; // Reset threshold
+ binding.recyclerView.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
+ galleryGridAdapter.notifyItemRangeChanged(0, galleryGridAdapter.getItemCount());
+ return true;
+ }
+ // Pinching In (Zooming Out) -> More Columns
+ else if (scaleFactor < 0.75f && currentSpans < MAX_COLUMNS) {
+ layoutManager.setSpanCount(currentSpans + 1);
+ scaleFactor = 1.0f; // Reset threshold
+ binding.recyclerView.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
+ galleryGridAdapter.notifyItemRangeChanged(0, galleryGridAdapter.getItemCount());
+ return true;
+ }
+ }
+ return false;
+ }
+ });
+
+ // Intercept touches on the RecyclerView to feed the detector
+ binding.recyclerView.setOnTouchListener((v, event) -> {
+ scaleGestureDetector.onTouchEvent(event);
+ return false; // Return false so normal scrolling still works perfectly
+ });
+ // -----------------------------
+
initViewModels();
}
@@ -253,4 +342,43 @@ private List findAllFilesInFolder(Uri uri) {
return files;
}
+ @Override
+ void showViewpager(boolean show, int pos, boolean animate) {
+ if (binding.layoutFabsAdd != null) binding.layoutFabsAdd.setVisibility(show ? View.GONE : View.VISIBLE);
+ View bottomNav = binding.getRoot().findViewById(R.id.bottom_navigation);
+ if (bottomNav != null) bottomNav.setVisibility(show ? View.GONE : View.VISIBLE);
+
+ if (show) {
+ binding.viewPager.setCurrentItem(pos, false);
+ binding.viewPager.setAlpha(0f);
+ binding.viewPager.setScaleX(0.95f);
+ binding.viewPager.setScaleY(0.95f);
+ binding.viewPager.setVisibility(View.VISIBLE);
+ galleryPagerAdapter.triggerActiveVideo(pos);
+
+ binding.viewPager.animate()
+ .alpha(1f)
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(250)
+ .setInterpolator(new androidx.interpolator.view.animation.FastOutSlowInInterpolator())
+ .start();
+ } else {
+ binding.viewPager.animate()
+ .alpha(0f)
+ .scaleX(0.95f)
+ .scaleY(0.95f)
+ .setDuration(200)
+ .setInterpolator(new androidx.interpolator.view.animation.FastOutSlowInInterpolator())
+ .withEndAction(() -> {
+ binding.viewPager.setVisibility(View.GONE);
+ binding.viewPager.setScaleX(1f);
+ binding.viewPager.setScaleY(1f);
+ binding.viewPager.setAlpha(1f);
+ })
+ .start();
+ }
+
+ super.showViewpager(show, pos, false);
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/se/arctosoft/vault/DirectoryBaseFragment.java b/app/src/main/java/se/arctosoft/vault/DirectoryBaseFragment.java
index b0625b8..ac1dcb5 100644
--- a/app/src/main/java/se/arctosoft/vault/DirectoryBaseFragment.java
+++ b/app/src/main/java/se/arctosoft/vault/DirectoryBaseFragment.java
@@ -347,7 +347,11 @@ void findFilesIn(Uri directoryUri) {
void setupGrid() {
initFastScroll();
- int spanCount = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE ? 6 : 3;
+ // Change the portrait span count from 3 to 4.
+ // You can also adjust the landscape (6) if you want it even wider when rotated!
+ int spanCount = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE ? 6 : 4;
+
+ // Notice it uses StaggeredGridLayoutManager here
RecyclerView.LayoutManager layoutManager = new StaggeredGridLayoutManager(spanCount, RecyclerView.VERTICAL);
binding.recyclerView.setLayoutManager(layoutManager);
galleryGridAdapter = new GalleryGridAdapter(requireActivity(), galleryViewModel.getGalleryFiles(), settings.showFilenames(), galleryViewModel.isRootDir(), galleryViewModel);
diff --git a/app/src/main/java/se/arctosoft/vault/DirectoryFragment.java b/app/src/main/java/se/arctosoft/vault/DirectoryFragment.java
index cca8524..22e2dda 100644
--- a/app/src/main/java/se/arctosoft/vault/DirectoryFragment.java
+++ b/app/src/main/java/se/arctosoft/vault/DirectoryFragment.java
@@ -7,6 +7,8 @@
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.util.Log;
+import android.view.HapticFeedbackConstants;
+import android.view.ScaleGestureDetector;
import android.view.View;
import androidx.activity.OnBackPressedCallback;
@@ -17,7 +19,9 @@
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.GridLayoutManager;
+import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.snackbar.Snackbar;
import java.io.FileNotFoundException;
@@ -41,6 +45,13 @@ public class DirectoryFragment extends DirectoryBaseFragment {
private Snackbar snackBarBackPressed;
private ShareViewModel shareViewModel;
+ private BottomNavigationView bottomNavigationView;
+
+ // --- NEW: Variables for the Breathing Grid ---
+ private ScaleGestureDetector scaleGestureDetector;
+ private float scaleFactor = 1.0f;
+ private static final int MIN_COLUMNS = 2;
+ private static final int MAX_COLUMNS = 6;
private final ActivityResultLauncher resultLauncherAddFolder = registerForActivityResult(new ActivityResultContracts.OpenDocumentTree(), uri -> {
if (uri != null) {
@@ -119,15 +130,24 @@ public void handleOnBackPressed() {
galleryGridAdapter.notifyItemChanged(pos);
});
+ // Initialize Bottom Navigation
+ bottomNavigationView = binding.getRoot().findViewById(R.id.bottom_navigation);
+
if (galleryViewModel.isRootDir()) {
setupViewpager();
setupGrid();
setClickListeners();
+ setupBottomNavigation();
if (!galleryViewModel.isInitialised()) {
addRootFolders();
}
} else {
+ // Hide bottom navigation if we are inside a specific folder
+ if (bottomNavigationView != null) {
+ bottomNavigationView.setVisibility(View.GONE);
+ }
+
DocumentFile documentFile = DocumentFile.fromSingleUri(context, galleryViewModel.getCurrentDirectoryUri());
if (documentFile != null && documentFile.isDirectory() && documentFile.exists()) {
setupViewpager();
@@ -144,6 +164,44 @@ public void handleOnBackPressed() {
}
}
+ // --- NEW: THE "BREATHING" GRID (PINCH TO ZOOM COLUMNS) ---
+ scaleGestureDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
+ @Override
+ public boolean onScale(@NonNull ScaleGestureDetector detector) {
+ scaleFactor *= detector.getScaleFactor();
+
+ if (binding.recyclerView.getLayoutManager() instanceof GridLayoutManager) {
+ GridLayoutManager layoutManager = (GridLayoutManager) binding.recyclerView.getLayoutManager();
+ int currentSpans = layoutManager.getSpanCount();
+
+ // Pinching Out (Zooming In) -> Fewer Columns
+ if (scaleFactor > 1.25f && currentSpans > MIN_COLUMNS) {
+ layoutManager.setSpanCount(currentSpans - 1);
+ scaleFactor = 1.0f; // Reset threshold
+ binding.recyclerView.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
+ galleryGridAdapter.notifyItemRangeChanged(0, galleryGridAdapter.getItemCount());
+ return true;
+ }
+ // Pinching In (Zooming Out) -> More Columns
+ else if (scaleFactor < 0.75f && currentSpans < MAX_COLUMNS) {
+ layoutManager.setSpanCount(currentSpans + 1);
+ scaleFactor = 1.0f; // Reset threshold
+ binding.recyclerView.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
+ galleryGridAdapter.notifyItemRangeChanged(0, galleryGridAdapter.getItemCount());
+ return true;
+ }
+ }
+ return false;
+ }
+ });
+
+ // Intercept touches on the RecyclerView to feed the detector
+ binding.recyclerView.setOnTouchListener((v, event) -> {
+ scaleGestureDetector.onTouchEvent(event);
+ return false; // Return false so normal scrolling still works perfectly
+ });
+ // -----------------------------
+
initViewModels();
shareViewModel = new ViewModelProvider(requireActivity()).get(ShareViewModel.class);
shareViewModel.getHasData().observe(getViewLifecycleOwner(), aBoolean -> {
@@ -153,10 +211,65 @@ public void handleOnBackPressed() {
});
}
+ private void setupBottomNavigation() {
+ if (bottomNavigationView == null) return;
+
+ bottomNavigationView.setVisibility(View.VISIBLE);
+ // Ensure "Albums" is selected when on this root screen
+ bottomNavigationView.setSelectedItemId(R.id.nav_albums);
+
+ bottomNavigationView.setOnItemSelectedListener(item -> {
+ int id = item.getItemId();
+ if (id == R.id.nav_all_files) {
+ // Navigate to the DirectoryAllFragment when "All Files" is clicked
+ navController.navigate(R.id.action_directory_to_directoryAll);
+ return true;
+ } else if (id == R.id.nav_albums) {
+ // Do nothing, we are already on the albums tab
+ return true;
+ }
+ return false;
+ });
+ }
+
@Override
void showViewpager(boolean show, int pos, boolean animate) {
- binding.layoutFabsAdd.setVisibility(show ? View.GONE : View.VISIBLE);
- super.showViewpager(show, pos, animate);
+ if (binding.layoutFabsAdd != null) binding.layoutFabsAdd.setVisibility(show ? View.GONE : View.VISIBLE);
+ View bottomNav = binding.getRoot().findViewById(R.id.bottom_navigation);
+ if (bottomNav != null) bottomNav.setVisibility(show ? View.GONE : View.VISIBLE);
+
+ if (show) {
+ binding.viewPager.setCurrentItem(pos, false);
+ binding.viewPager.setAlpha(0f);
+ binding.viewPager.setScaleX(0.95f);
+ binding.viewPager.setScaleY(0.95f);
+ binding.viewPager.setVisibility(View.VISIBLE);
+ galleryPagerAdapter.triggerActiveVideo(pos);
+
+ binding.viewPager.animate()
+ .alpha(1f)
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(250)
+ .setInterpolator(new androidx.interpolator.view.animation.FastOutSlowInInterpolator())
+ .start();
+ } else {
+ binding.viewPager.animate()
+ .alpha(0f)
+ .scaleX(0.95f)
+ .scaleY(0.95f)
+ .setDuration(200)
+ .setInterpolator(new androidx.interpolator.view.animation.FastOutSlowInInterpolator())
+ .withEndAction(() -> {
+ binding.viewPager.setVisibility(View.GONE);
+ binding.viewPager.setScaleX(1f);
+ binding.viewPager.setScaleY(1f);
+ binding.viewPager.setAlpha(1f);
+ })
+ .start();
+ }
+
+ super.showViewpager(show, pos, false);
}
private void checkSharedData() {
@@ -183,7 +296,6 @@ private void setClickListeners() {
if (expandedFabs) {
binding.fab.animate().rotation(0).setDuration(120).start();
for (View view : views) {
- //view.animate().alpha(0f).setDuration(120).setListener(getHideOnEndListener(view)).start();
view.setAlpha(0f);
view.setVisibility(View.GONE);
}
@@ -238,7 +350,7 @@ private void setClickListeners() {
}
});
binding.fabImportMedia.setOnClickListener(v -> {
- String[] mimeTypes = new String[]{"image/*", "video/*"};
+ String[] mimeTypes = new String[]{"image/*", "video/*", "audio/*"};
resultLauncherOpenDocuments.launch(mimeTypes);
binding.fab.performClick();
});
@@ -326,6 +438,12 @@ void onSelectionModeChanged(boolean inSelectionMode) {
binding.layoutFabsAdd.setVisibility(View.VISIBLE);
binding.layoutFabsRemoveFolders.setVisibility(View.GONE);
}
+
+ // Hide bottom navigation during selection mode to prevent weird UX
+ if (bottomNavigationView != null && galleryViewModel.isRootDir()) {
+ bottomNavigationView.setVisibility(inSelectionMode ? View.GONE : View.VISIBLE);
+ }
+
requireActivity().invalidateOptionsMenu();
}
@@ -341,13 +459,9 @@ private void addFolder(Uri uri, boolean asRootDir) {
public void onAddedAsRoot() {
Toaster.getInstance(context).showLong(getString(R.string.gallery_added_folder, FileStuff.getFilenameWithPathFromUri(uri)));
Uri directoryUri = documentFile.getUri();
- //List galleryFiles = FileStuff.getFilesInFolder(context, directoryUri);
- if (galleryViewModel.getGalleryFiles().isEmpty()) {
- addAllFolder();
- }
synchronized (LOCK) {
- galleryViewModel.getGalleryFiles().add(0, GalleryFile.asDirectory(directoryUri/*, galleryFiles*/));
+ galleryViewModel.getGalleryFiles().add(0, GalleryFile.asDirectory(directoryUri));
galleryGridAdapter.notifyItemInserted(0);
}
}
@@ -365,12 +479,6 @@ public void onAlreadyExists() {
}
}
});
- //if (viewModel.getFilesToAdd() != null) {
- // importFiles(viewModel.getFilesToAdd());
- //}
- //if (viewModel.getTextToImport() != null) {
- // importText(viewModel.getTextToImport());
- //}
}
@Override
@@ -411,23 +519,12 @@ private void addFoundRootDirectories(@NonNull List directories, FragmentAct
});
}
activity.runOnUiThread(() -> {
- if (navController.getPreviousBackStackEntry() == null && !galleryViewModel.getGalleryFiles().isEmpty()) {
- addAllFolder();
- }
binding.noMedia.setVisibility(directories.isEmpty() ? View.VISIBLE : View.GONE);
setLoading(false);
});
galleryViewModel.setInitialised(true);
}
- private void addAllFolder() {
- synchronized (LOCK) {
- galleryViewModel.getGalleryFiles().add(0, GalleryFile.asAllFolder(getString(R.string.gallery_all)));
- galleryGridAdapter.notifyItemInserted(0);
- }
- binding.noMedia.setVisibility(View.GONE);
- }
-
@Override
public void onStart() {
super.onStart();
diff --git a/app/src/main/java/se/arctosoft/vault/adapters/GalleryGridAdapter.java b/app/src/main/java/se/arctosoft/vault/adapters/GalleryGridAdapter.java
index 7ee1a53..a66e4e1 100644
--- a/app/src/main/java/se/arctosoft/vault/adapters/GalleryGridAdapter.java
+++ b/app/src/main/java/se/arctosoft/vault/adapters/GalleryGridAdapter.java
@@ -24,14 +24,17 @@
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.util.Log;
+import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.animation.OvershootInterpolator;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.res.ResourcesCompat;
+import androidx.core.view.ViewCompat;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity;
import androidx.navigation.Navigation;
@@ -66,7 +69,6 @@
import se.arctosoft.vault.utils.Dialogs;
import se.arctosoft.vault.utils.GlideStuff;
import se.arctosoft.vault.utils.Settings;
-import se.arctosoft.vault.utils.StringStuff;
import se.arctosoft.vault.viewmodel.GalleryViewModel;
public class GalleryGridAdapter extends RecyclerView.Adapter implements IOnSelectionModeChanged, FastScrollRecyclerView.SectionedAdapter {
@@ -80,6 +82,9 @@ public class GalleryGridAdapter extends RecyclerView.Adapter weakReference;
private final List galleryFiles;
private final UniqueLinkedList selectedFiles;
@@ -100,7 +105,7 @@ public record Payload(int type) {
static final int TYPE_TOGGLE_FILENAME = 1;
static final int TYPE_NEW_FILENAME = 2;
static final int TYPE_LOADED_NOTE = 3;
- public static final int TYPE_RELEASE_VIDEO = 4;
+ public static final int TYPE_RELEASE_VIDEO = 4;
}
public GalleryGridAdapter(FragmentActivity context, @NonNull List galleryFiles, boolean showFileNames, boolean isRootDir, GalleryViewModel galleryViewModel) {
@@ -144,27 +149,43 @@ public void onBindViewHolder(@NonNull GalleryGridViewHolder holder, int position
GalleryFile galleryFile = galleryFiles.get(position);
updateSelectedView(holder, galleryFile);
- holder.binding.txtName.setVisibility(showFileNames || galleryFile.isDirectory() ? View.VISIBLE : View.GONE);
+
+ boolean showText = showFileNames || galleryFile.isDirectory();
+ holder.binding.txtName.setVisibility(showText ? View.VISIBLE : View.GONE);
+ holder.binding.txtSize.setVisibility(showText ? View.VISIBLE : View.GONE);
+
holder.binding.imageView.setImageDrawable(null);
- if (!isRootDir && (galleryFile.isGif() || galleryFile.isVideo() || galleryFile.isDirectory())) {
+
+ // Set Transition Name for Shared Element Animations
+ ViewCompat.setTransitionName(holder.binding.imageView, galleryFile.getUri().toString());
+
+ // --- NEW: Audio Support in the Top Corner Type Indicator ---
+ if (!isRootDir && (galleryFile.isGif() || galleryFile.isVideo() || galleryFile.isAudio() || galleryFile.isDirectory())) {
holder.binding.imgType.setVisibility(View.VISIBLE);
- holder.binding.imgType.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), galleryFile.isGif()
- ? R.drawable.ic_round_gif_24 : (galleryFile.isVideo()
- ? R.drawable.ic_outline_video_file_24 : (galleryFile.isText() ? R.drawable.outline_text_snippet_24 : R.drawable.ic_round_folder_open_24)),
- context.getTheme()));
+
+ int iconRes = R.drawable.ic_round_folder_open_24;
+ if (galleryFile.isGif()) iconRes = R.drawable.ic_round_gif_24;
+ else if (galleryFile.isVideo()) iconRes = R.drawable.ic_outline_video_file_24;
+ else if (galleryFile.isAudio()) iconRes = R.drawable.ic_outline_audio_file_24;
+ else if (galleryFile.isText()) iconRes = R.drawable.outline_text_snippet_24;
+
+ holder.binding.imgType.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), iconRes, context.getTheme()));
} else {
holder.binding.imgType.setVisibility(View.GONE);
}
+
holder.binding.hasDescription.setVisibility(!isRootDir && galleryFile.hasNote() ? View.VISIBLE : View.GONE);
holder.binding.imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
holder.binding.textView.setVisibility(View.GONE);
holder.binding.textView.setText(null);
+
if (galleryFile.isAllFolder()) {
holder.binding.imageView.setVisibility(View.VISIBLE);
holder.binding.imageView.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.round_all_inclusive_24, context.getTheme()));
holder.binding.imageView.setScaleType(ImageView.ScaleType.CENTER);
holder.binding.txtName.setText(context.getString(R.string.gallery_all));
+ holder.binding.txtSize.setVisibility(View.GONE);
} else if (galleryFile.isDirectory()) {
holder.binding.imageView.setVisibility(View.VISIBLE);
galleryFile.findFilesInDirectory(context, () -> {
@@ -186,7 +207,11 @@ public void onBindViewHolder(@NonNull GalleryGridViewHolder holder, int position
.apply(GlideStuff.getRequestOptions(useDiskCache))
.into(holder.binding.imageView);
}
- holder.binding.txtName.setText(context.getString(R.string.gallery_adapter_folder_name, galleryFile.getNameWithPath(), galleryFile.getFileCount()));
+
+ String cleanFolderName = new java.io.File(galleryFile.getNameWithPath()).getName();
+ holder.binding.txtName.setText(cleanFolderName);
+ holder.binding.txtSize.setText(galleryFile.getFileCount() + " Items");
+
} else if (galleryFile.isText()) {
holder.binding.imageView.setVisibility(View.GONE);
holder.binding.textView.setText(galleryFile.getText() == null ? context.getString(R.string.loading) : galleryFile.getText());
@@ -195,8 +220,15 @@ public void onBindViewHolder(@NonNull GalleryGridViewHolder holder, int position
if (galleryFile.getText() == null) {
readText(context, galleryFile, holder);
}
+ // --- NEW: Audio Thumbnail Support ---
+ } else if (galleryFile.isAudio()) {
+ holder.binding.imageView.setVisibility(View.VISIBLE);
+ Glide.with(context)
+ .load(R.drawable.ic_outline_audio_file_24)
+ .centerInside() // Keeps the icon neatly in the center instead of stretching it
+ .into(holder.binding.imageView);
+ setItemFilename(holder, context, galleryFile);
} else {
- //Log.e(TAG, "onBindViewHolder: load image, version " + galleryFile.getVersion() + ", " + galleryFile.getFileType().suffixPrefix);
holder.binding.imageView.setVisibility(View.VISIBLE);
if (galleryFile.getThumbUri() != null) {
Glide.with(context)
@@ -247,19 +279,62 @@ private void readText(FragmentActivity context, GalleryFile galleryFile, Gallery
}
private void setItemFilename(@NonNull GalleryGridViewHolder holder, Context context, @NonNull GalleryFile galleryFile) {
- if (galleryFile.getSize() > 0) {
- holder.binding.txtName.setText(context.getString(R.string.gallery_adapter_file_name, galleryFile.getName(), StringStuff.bytesToReadableString(galleryFile.getSize())));
+ if (galleryFile.isDirectory()) {
+ String cleanFolderName = new java.io.File(galleryFile.getNameWithPath()).getName();
+ holder.binding.txtName.setText(cleanFolderName);
+ holder.binding.txtSize.setVisibility(View.VISIBLE);
+ holder.binding.txtSize.setText(galleryFile.getFileCount() + " Items");
+ return;
+ }
+
+ if (galleryFile.getOriginalName() != null) {
+ displayFileInfo(holder, context, galleryFile.getOriginalName(), galleryFile.getSize());
} else {
- holder.binding.txtName.setText(galleryFile.getName());
+ displayFileInfo(holder, context, galleryFile.getName(), galleryFile.getSize());
+
+ nameDecryptionExecutor.execute(() -> {
+ try {
+ String realName = Encryption.getOriginalFilename(context.getContentResolver().openInputStream(galleryFile.getUri()), password.getPassword(), false, galleryFile.getVersion());
+ galleryFile.setOriginalName(realName);
+
+ if (context instanceof FragmentActivity) {
+ ((FragmentActivity) context).runOnUiThread(() -> {
+ if (holder.getBindingAdapterPosition() != RecyclerView.NO_POSITION &&
+ galleryFiles.get(holder.getBindingAdapterPosition()) == galleryFile) {
+ displayFileInfo(holder, context, realName, galleryFile.getSize());
+ }
+ });
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+ }
+ }
+
+ private void displayFileInfo(GalleryGridViewHolder holder, Context context, String name, long size) {
+ holder.binding.txtName.setText(name);
+
+ if (size > 0) {
+ holder.binding.txtSize.setVisibility(View.VISIBLE);
+ String formattedSize = android.text.format.Formatter.formatShortFileSize(context, size);
+ holder.binding.txtSize.setText(formattedSize);
+ } else {
+ holder.binding.txtSize.setVisibility(View.GONE);
}
}
private void setClickListener(@NonNull GalleryGridViewHolder holder, FragmentActivity context, GalleryFile galleryFile) {
holder.binding.layout.setOnClickListener(v -> {
final int pos = holder.getBindingAdapterPosition();
+
+ if (selectMode) {
+ v.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
+ }
+
if (galleryFile.isAllFolder()) {
if (!selectMode) {
- Navigation.findNavController(holder.binding.layout).navigate(R.id.action_directory_to_directory_all);
+ Navigation.findNavController(holder.binding.layout).navigate(R.id.action_directory_to_directoryAll);
}
} else if (selectMode) {
if (isRootDir || !galleryFile.isDirectory()) {
@@ -296,7 +371,10 @@ private void setClickListener(@NonNull GalleryGridViewHolder holder, FragmentAct
}
}
});
+
holder.binding.layout.setOnLongClickListener(v -> {
+ v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+
if (!galleryFile.isAllFolder() && galleryFile.isDirectory() && galleryFile.getFindFilesInDirectoryStatus() == GalleryFile.FIND_FILES_DONE
&& galleryFile.getFileCount() == 0) {
Dialogs.showConfirmationDialog(context, context.getString(R.string.gallery_delete_folder_title), context.getString(R.string.gallery_delete_folder_message), (dialogInterface, i) -> {
@@ -333,9 +411,6 @@ private void setClickListener(@NonNull GalleryGridViewHolder holder, FragmentAct
}
notifyItemRangeChanged(minPos, 1 + (maxPos - minPos), new Payload(Payload.TYPE_SELECT_ALL));
}
- //if (context instanceof GalleryDirectoryActivity activity) {
- // activity.onSelectionChanged(selectedFiles.size());
- //}
} else {
holder.binding.layout.performClick();
}
@@ -357,7 +432,9 @@ public void onBindViewHolder(@NonNull GalleryGridViewHolder holder, int position
break;
} else if (((Payload) o).type == Payload.TYPE_TOGGLE_FILENAME) {
GalleryFile galleryFile = galleryFiles.get(position);
- holder.binding.txtName.setVisibility(showFileNames || galleryFile.isDirectory() ? View.VISIBLE : View.GONE);
+ boolean showText = showFileNames || galleryFile.isDirectory();
+ holder.binding.txtName.setVisibility(showText ? View.VISIBLE : View.GONE);
+ holder.binding.txtSize.setVisibility(showText ? View.VISIBLE : View.GONE);
found = true;
} else if (((Payload) o).type == Payload.TYPE_NEW_FILENAME) {
setItemFilename(holder, weakReference.get(), galleryFiles.get(holder.getBindingAdapterPosition()));
@@ -387,11 +464,29 @@ private void setSelectMode(boolean selectionMode) {
private void updateSelectedView(GalleryGridViewHolder holder, GalleryFile galleryFile) {
if (!galleryFile.isAllFolder() && selectMode && (isRootDir || !galleryFile.isDirectory())) {
+ boolean isSelected = selectedFiles.contains(galleryFile);
holder.binding.checked.setVisibility(View.VISIBLE);
- holder.binding.checked.setChecked(selectedFiles.contains(galleryFile));
+ holder.binding.checked.setChecked(isSelected);
+
+ // Bounce it slightly inward when selected
+ float scale = isSelected ? 0.88f : 1.0f;
+ holder.binding.cardImage.animate()
+ .scaleX(scale)
+ .scaleY(scale)
+ .setDuration(250)
+ .setInterpolator(new OvershootInterpolator())
+ .start();
} else {
holder.binding.checked.setVisibility(View.GONE);
holder.binding.checked.setChecked(false);
+
+ // Return to full size
+ holder.binding.cardImage.animate()
+ .scaleX(1.0f)
+ .scaleY(1.0f)
+ .setDuration(250)
+ .setInterpolator(new OvershootInterpolator())
+ .start();
}
}
@@ -437,9 +532,6 @@ public void selectAll() {
}
notifyItemRangeChanged(0, galleryFiles.size(), new Payload(Payload.TYPE_SELECT_ALL));
}
- //if (weakReference.get() instanceof GalleryDirectoryActivity activity) {
- // activity.onSelectionChanged(selectedFiles.size());
- //}
}
public boolean toggleFilenames() {
@@ -452,4 +544,4 @@ public boolean toggleFilenames() {
public List getSelectedFiles() {
return selectedFiles;
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/se/arctosoft/vault/adapters/GalleryPagerAdapter.java b/app/src/main/java/se/arctosoft/vault/adapters/GalleryPagerAdapter.java
index 6d21218..4c353c3 100644
--- a/app/src/main/java/se/arctosoft/vault/adapters/GalleryPagerAdapter.java
+++ b/app/src/main/java/se/arctosoft/vault/adapters/GalleryPagerAdapter.java
@@ -18,22 +18,30 @@
package se.arctosoft.vault.adapters;
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
import android.content.ContentResolver;
+import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.PointF;
+import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
+import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
+import android.widget.ImageButton;
+import android.widget.TextView;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.PopupMenu;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.FileProvider;
@@ -55,6 +63,8 @@
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.target.CustomTarget;
+import com.bumptech.glide.request.transition.Transition;
import com.google.android.material.color.MaterialColors;
import org.json.JSONException;
@@ -141,15 +151,19 @@ public GalleryPagerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
AdapterGalleryViewpagerItemBinding parentBinding = AdapterGalleryViewpagerItemBinding.inflate(layoutInflater, parent, false);
setPadding(parentBinding);
+
if (viewType == FileType.TYPE_IMAGE) {
AdapterGalleryViewpagerItemImageBinding imageBinding = AdapterGalleryViewpagerItemImageBinding.inflate(layoutInflater, parentBinding.content, true);
return new GalleryPagerViewHolder.GalleryPagerImageViewHolder(parentBinding, imageBinding);
} else if (viewType == FileType.TYPE_GIF) {
AdapterGalleryViewpagerItemGifBinding gifBinding = AdapterGalleryViewpagerItemGifBinding.inflate(layoutInflater, parentBinding.content, true);
return new GalleryPagerViewHolder.GalleryPagerGifViewHolder(parentBinding, gifBinding);
- } else if (viewType == FileType.TYPE_VIDEO) {
+
+ // --- NEW: Audio routes into the Video View Holder! ---
+ } else if (viewType == FileType.TYPE_VIDEO || viewType == FileType.TYPE_AUDIO) {
AdapterGalleryViewpagerItemVideoBinding videoBinding = AdapterGalleryViewpagerItemVideoBinding.inflate(layoutInflater, parentBinding.content, true);
return new GalleryPagerViewHolder.GalleryPagerVideoViewHolder(parentBinding, videoBinding);
+
} else if (viewType == FileType.TYPE_TEXT) {
AdapterGalleryViewpagerItemTextBinding textBinding = AdapterGalleryViewpagerItemTextBinding.inflate(layoutInflater, parentBinding.content, true);
setViewPadding(textBinding.text);
@@ -200,15 +214,20 @@ public void onBindViewHolder(@NonNull GalleryPagerViewHolder holder, int positio
if (holder instanceof GalleryPagerViewHolder.GalleryPagerDirectoryViewHolder) {
setupDirectoryView(holder, context, galleryFile);
} else {
- holder.parentBinding.txtName.setVisibility(View.VISIBLE);
+ // Force the top name to be permanently hidden
+ holder.parentBinding.txtName.setVisibility(View.GONE);
holder.parentBinding.lLButtons.setVisibility(View.VISIBLE);
- setName(holder, galleryFile);
+
+ // --- NEW: Apply Palette Chameleon Colors ---
+ applyDynamicChameleonColor(context, holder, galleryFile.getThumbUri());
+
if (holder instanceof GalleryPagerViewHolder.GalleryPagerVideoViewHolder) {
holder.parentBinding.imgFullscreen.setVisibility(View.VISIBLE);
setupVideoView((GalleryPagerViewHolder.GalleryPagerVideoViewHolder) holder, context, galleryFile);
} else if (holder instanceof GalleryPagerViewHolder.GalleryPagerTextViewHolder) {
holder.parentBinding.imgFullscreen.setVisibility(View.VISIBLE);
setupTextView((GalleryPagerViewHolder.GalleryPagerTextViewHolder) holder, context, galleryFile);
+ attachPullToDismiss(((GalleryPagerViewHolder.GalleryPagerTextViewHolder) holder).binding.text, holder.parentBinding.content, context);
} else {
holder.parentBinding.imgFullscreen.setVisibility(View.GONE);
setupImageView(holder, context, galleryFile);
@@ -219,21 +238,122 @@ public void onBindViewHolder(@NonNull GalleryPagerViewHolder holder, int positio
}
}
+ // --- NEW: Pull to Dismiss Logic ---
+ private void attachPullToDismiss(View touchView, View animateView, FragmentActivity context) {
+ touchView.setOnTouchListener(new View.OnTouchListener() {
+ float startY = 0;
+ float startX = 0;
+ boolean isDragging = false;
+ boolean isHandlingTouch = false;
+
+ @Override
+ public boolean onTouch(View v, android.view.MotionEvent event) {
+ if (touchView instanceof MySubsamplingScaleImageView) {
+ MySubsamplingScaleImageView img = (MySubsamplingScaleImageView) touchView;
+ if (img.getScale() > img.getMinScale() + 0.05f) {
+ return false;
+ }
+ }
+
+ switch (event.getAction()) {
+ case android.view.MotionEvent.ACTION_DOWN:
+ startY = event.getRawY();
+ startX = event.getRawX();
+ isDragging = false;
+ isHandlingTouch = true;
+ return false;
+
+ case android.view.MotionEvent.ACTION_MOVE:
+ if (!isHandlingTouch) return false;
+ float deltaY = event.getRawY() - startY;
+ float deltaX = event.getRawX() - startX;
+
+ if (!isDragging && Math.abs(deltaX) > Math.abs(deltaY)) {
+ isHandlingTouch = false;
+ return false;
+ }
+
+ if (!isDragging && deltaY > 150) {
+ isDragging = true;
+ v.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
+ }
+
+ if (isDragging) {
+ float screenHeight = v.getHeight();
+ float scale = 1f - (Math.abs(deltaY) / (screenHeight * 1.5f));
+ scale = Math.max(0.5f, scale);
+ animateView.setScaleX(scale);
+ animateView.setScaleY(scale);
+ animateView.setTranslationY(deltaY);
+ return true;
+ }
+ break;
+
+ case android.view.MotionEvent.ACTION_UP:
+ case android.view.MotionEvent.ACTION_CANCEL:
+ if (isDragging) {
+ float deltaYUp = event.getRawY() - startY;
+ if (deltaYUp > v.getHeight() * 0.20f) {
+ context.onBackPressed();
+ } else {
+ animateView.animate()
+ .scaleX(1f).scaleY(1f).translationY(0)
+ .setDuration(250)
+ .setInterpolator(new androidx.interpolator.view.animation.FastOutSlowInInterpolator())
+ .start();
+ }
+ isDragging = false;
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+ });
+ }
+
+ // --- NEW: Chameleon Background Colors ---
+ private void applyDynamicChameleonColor(FragmentActivity context, GalleryPagerViewHolder holder, Uri uri) {
+ if (uri == null) return;
+ Glide.with(context)
+ .asBitmap()
+ .load(uri)
+ .apply(GlideStuff.getRequestOptions(useDiskCache))
+ .into(new CustomTarget() {
+ @Override
+ public void onResourceReady(@NonNull android.graphics.Bitmap resource, @Nullable Transition super android.graphics.Bitmap> transition) {
+ androidx.palette.graphics.Palette.from(resource).generate(palette -> {
+ if (palette != null) {
+ int defaultColor = context.getResources().getColor(R.color.black, context.getTheme());
+ int dominantColor = palette.getDarkMutedColor(defaultColor);
+
+ // Save the extracted color
+ holder.parentBinding.getRoot().setTag(dominantColor);
+
+ // Smoothly animate to it if we are in fullscreen mode
+ if (isFullscreen) {
+ ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), defaultColor, dominantColor);
+ colorAnimation.setDuration(400);
+ colorAnimation.addUpdateListener(animator -> holder.parentBinding.getRoot().setBackgroundColor((int) animator.getAnimatedValue()));
+ colorAnimation.start();
+ }
+ }
+ });
+ }
+ @Override
+ public void onLoadCleared(@Nullable Drawable placeholder) {}
+ });
+ }
+
private void setupDirectoryView(@NonNull GalleryPagerViewHolder holder, FragmentActivity context, GalleryFile galleryFile) {
holder.parentBinding.lLButtons.setVisibility(View.GONE);
holder.parentBinding.imgFullscreen.setVisibility(View.GONE);
- holder.parentBinding.noteLayout.setVisibility(View.GONE);
holder.parentBinding.txtName.setVisibility(View.GONE);
- ((GalleryPagerViewHolder.GalleryPagerDirectoryViewHolder) holder).binding.name.setText(context.getString(R.string.gallery_click_to_open_directory, galleryFile.getNameWithPath()));
+
+ String folderName = new java.io.File(galleryFile.getNameWithPath()).getName();
+ ((GalleryPagerViewHolder.GalleryPagerDirectoryViewHolder) holder).binding.name.setText(context.getString(R.string.gallery_click_to_open_directory, folderName));
+
((GalleryPagerViewHolder.GalleryPagerDirectoryViewHolder) holder).binding.getRoot().setOnClickListener(v -> {
- /*Intent intent = new Intent(context, GalleryDirectoryActivity.class);
- if (nestedPath != null) {
- intent.putExtra(GalleryDirectoryActivity.EXTRA_DIRECTORY, galleryFile.getUri().toString())
- .putExtra(GalleryDirectoryActivity.EXTRA_NESTED_PATH, nestedPath + "/" + new File(galleryFile.getUri().getPath()).getName());
- } else {
- intent.putExtra(GalleryDirectoryActivity.EXTRA_DIRECTORY, galleryFile.getUri().toString());
- }
- context.startActivity(intent);*/
Bundle bundle = new Bundle();
if (nestedPath != null) {
bundle.putString(DirectoryFragment.ARGUMENT_DIRECTORY, galleryFile.getUri().toString());
@@ -254,7 +374,8 @@ private void setupDirectoryView(@NonNull GalleryPagerViewHolder holder, Fragment
}
private void setName(@NonNull GalleryPagerViewHolder holder, GalleryFile galleryFile) {
- holder.parentBinding.txtName.setText(weakReference.get().getString(R.string.gallery_adapter_file_name, galleryFile.getName(), StringStuff.bytesToReadableString(galleryFile.getSize())));
+ String displayName = galleryFile.getOriginalName() != null ? galleryFile.getOriginalName() : galleryFile.getName();
+ holder.parentBinding.txtName.setText(weakReference.get().getString(R.string.gallery_adapter_file_name, displayName, StringStuff.bytesToReadableString(galleryFile.getSize())));
}
@Override
@@ -314,18 +435,241 @@ private void setupTextView(GalleryPagerViewHolder.GalleryPagerTextViewHolder hol
holder.binding.text.setTextIsSelectable(true);
}
+ @OptIn(markerClass = UnstableApi.class)
private void setupVideoView(GalleryPagerViewHolder.GalleryPagerVideoViewHolder holder, FragmentActivity context, GalleryFile galleryFile) {
- holder.binding.rLPlay.setVisibility(View.VISIBLE);
- holder.binding.playerView.setVisibility(View.INVISIBLE);
- Glide.with(context)
- .load(galleryFile.getThumbUri())
- .apply(GlideStuff.getRequestOptions(useDiskCache))
- .into(holder.binding.imgThumb);
- holder.parentBinding.imgFullscreen.setVisibility(isFullscreen ? View.GONE : View.VISIBLE);
- holder.binding.rLPlay.setOnClickListener(v -> {
- holder.binding.rLPlay.setVisibility(View.GONE);
- holder.binding.playerView.setVisibility(View.VISIBLE);
- playVideo(context, galleryFile.getUri(), holder, galleryFile.getVersion(), galleryViewModel.getVideoPosition(galleryFile.getUri()));
+ // Frictionless UI: Hide the play button overlay, show the player immediately
+ holder.binding.rLPlay.setVisibility(View.GONE);
+ holder.binding.playerView.setVisibility(View.VISIBLE);
+ holder.parentBinding.txtName.setVisibility(View.GONE);
+
+ // Audio Thumbnail Injection
+ if (galleryFile.isAudio()) {
+ Glide.with(context)
+ .load(R.drawable.ic_outline_audio_file_24)
+ .centerInside()
+ .into(holder.binding.imgThumb);
+ } else {
+ Glide.with(context)
+ .load(galleryFile.getThumbUri())
+ .apply(GlideStuff.getRequestOptions(useDiskCache))
+ .into(holder.binding.imgThumb);
+ }
+
+ View controllerView = holder.binding.playerView;
+ View gestureOverlay = controllerView.findViewById(R.id.gesture_overlay);
+ TextView tvGestureText = controllerView.findViewById(R.id.tv_gesture_text);
+
+ Runnable hideOverlay = () -> {
+ if (gestureOverlay != null) {
+ gestureOverlay.animate().alpha(0f).setDuration(250).withEndAction(() -> gestureOverlay.setVisibility(View.GONE));
+ }
+ };
+
+ TextView btnAspectRatio = controllerView.findViewById(R.id.btnAspectRatio);
+ if (btnAspectRatio != null) {
+ btnAspectRatio.setOnClickListener(v -> {
+ int currentMode = holder.binding.playerView.getResizeMode();
+ if (currentMode == androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT) {
+ holder.binding.playerView.setResizeMode(androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM);
+ btnAspectRatio.setText("ZOOM");
+ } else if (currentMode == androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM) {
+ holder.binding.playerView.setResizeMode(androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL);
+ btnAspectRatio.setText("FILL");
+ } else {
+ holder.binding.playerView.setResizeMode(androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT);
+ btnAspectRatio.setText("FIT");
+ }
+ });
+ }
+
+ TextView btnRotate = controllerView.findViewById(R.id.btnRotate);
+ if (btnRotate != null) {
+ btnRotate.setOnClickListener(v -> {
+ int currentOrientation = context.getResources().getConfiguration().orientation;
+ if (currentOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) {
+ context.setRequestedOrientation(android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ } else {
+ context.setRequestedOrientation(android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
+ }
+ });
+ }
+
+ ImageButton btnPlayPause = controllerView.findViewById(R.id.custom_play_pause);
+ if (btnPlayPause != null) {
+ btnPlayPause.setOnClickListener(v -> {
+ int pos = holder.getBindingAdapterPosition();
+ if (pos >= 0) {
+ ExoPlayer player = players.get(pos);
+ if (player != null) {
+ if (player.isPlaying()) player.pause();
+ else player.play();
+ btnPlayPause.setImageResource(player.isPlaying() ? R.drawable.ic_baseline_pause_24 : R.drawable.ic_baseline_play_arrow_24);
+ }
+ }
+ });
+ }
+
+ final android.media.AudioManager audioManager = (android.media.AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+
+ // --- Video Touch Engine (Combined Gestures + Haptics + Pull-to-Dismiss) ---
+ holder.binding.playerView.setOnTouchListener(new View.OnTouchListener() {
+ private float startY = 0f;
+ private float startX = 0f;
+ private int startVolume = 0;
+ private float startBrightness = 0f;
+ private boolean isRightSide = false;
+
+ // Haptic Trackers
+ private int lastHapticVolume = -1;
+ private int lastHapticBrightness = -1;
+ private boolean isPullingToDismiss = false;
+
+ private final android.view.GestureDetector gestureDetector = new android.view.GestureDetector(context, new android.view.GestureDetector.SimpleOnGestureListener() {
+
+ @Override
+ public boolean onDown(android.view.MotionEvent e) {
+ startY = e.getRawY();
+ startX = e.getRawX();
+ isRightSide = e.getX() > (holder.binding.playerView.getWidth() / 2f);
+ startVolume = audioManager.getStreamVolume(android.media.AudioManager.STREAM_MUSIC);
+ android.view.Window window = context.getWindow();
+ startBrightness = window.getAttributes().screenBrightness;
+ if (startBrightness < 0) startBrightness = 0.5f;
+
+ lastHapticVolume = startVolume;
+ lastHapticBrightness = (int)(startBrightness * 100);
+ isPullingToDismiss = false;
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(android.view.MotionEvent e) {
+ if (holder.binding.playerView.isControllerFullyVisible()) {
+ holder.binding.playerView.hideController();
+ } else {
+ holder.binding.playerView.showController();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onDoubleTap(android.view.MotionEvent e) {
+ int pos = holder.getBindingAdapterPosition();
+ if (pos >= 0) {
+ ExoPlayer player = players.get(pos);
+ if (player != null) {
+ long currentPos = player.getCurrentPosition();
+
+ // Tactile feedback on Seek
+ holder.binding.playerView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+
+ gestureOverlay.animate().cancel();
+ gestureOverlay.setVisibility(View.VISIBLE);
+ gestureOverlay.setAlpha(1f);
+
+ if (e.getX() > (holder.binding.playerView.getWidth() / 2f)) {
+ player.seekTo(Math.min(player.getDuration(), currentPos + 10000));
+ tvGestureText.setText("⏩ +10s");
+ } else {
+ player.seekTo(Math.max(0, currentPos - 10000));
+ tvGestureText.setText("⏪ -10s");
+ }
+
+ holder.binding.playerView.removeCallbacks(hideOverlay);
+ holder.binding.playerView.postDelayed(hideOverlay, 800);
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onScroll(android.view.MotionEvent e1, android.view.MotionEvent e2, float distanceX, float distanceY) {
+ float deltaY = e2.getRawY() - startY;
+ float deltaX = e2.getRawX() - startX;
+
+ // Pull-to-dismiss integration
+ if (!isPullingToDismiss && deltaY > 150 && Math.abs(deltaY) > Math.abs(deltaX)) {
+ isPullingToDismiss = true;
+ holder.binding.playerView.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
+ }
+
+ if (isPullingToDismiss) {
+ float screenHeight = holder.binding.playerView.getHeight();
+ float scale = 1f - (deltaY / (screenHeight * 1.5f));
+ scale = Math.max(0.5f, scale);
+ holder.parentBinding.content.setScaleX(scale);
+ holder.parentBinding.content.setScaleY(scale);
+ holder.parentBinding.content.setTranslationY(deltaY);
+ return true;
+ }
+
+ // Otherwise, execute Volume/Brightness logic
+ if (Math.abs(deltaX) > Math.abs(deltaY)) return false;
+
+ float swipePercentage = (startY - e2.getRawY()) / holder.binding.playerView.getHeight();
+
+ gestureOverlay.animate().cancel();
+ gestureOverlay.setVisibility(View.VISIBLE);
+ gestureOverlay.setAlpha(1f);
+
+ if (isRightSide) {
+ int maxVolume = audioManager.getStreamMaxVolume(android.media.AudioManager.STREAM_MUSIC);
+ int volumeChange = (int) (maxVolume * swipePercentage);
+ int newVolume = Math.max(0, Math.min(maxVolume, startVolume + volumeChange));
+ audioManager.setStreamVolume(android.media.AudioManager.STREAM_MUSIC, newVolume, 0);
+
+ // Only tick when volume changes
+ if (newVolume != lastHapticVolume) {
+ holder.binding.playerView.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
+ lastHapticVolume = newVolume;
+ }
+
+ int displayVol = (int) (((float) newVolume / maxVolume) * 100);
+ tvGestureText.setText("🔊 " + displayVol + "%");
+ } else {
+ android.view.Window window = context.getWindow();
+ android.view.WindowManager.LayoutParams lp = window.getAttributes();
+ float newBrightness = Math.max(0.01f, Math.min(1.0f, startBrightness + swipePercentage));
+ lp.screenBrightness = newBrightness;
+ window.setAttributes(lp);
+
+ int brightPercent = (int) (newBrightness * 100);
+ if (Math.abs(brightPercent - lastHapticBrightness) >= 3) {
+ holder.binding.playerView.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
+ lastHapticBrightness = brightPercent;
+ }
+
+ tvGestureText.setText("☀️ " + brightPercent + "%");
+ }
+
+ holder.binding.playerView.removeCallbacks(hideOverlay);
+ holder.binding.playerView.postDelayed(hideOverlay, 800);
+
+ return true;
+ }
+ });
+
+ @Override
+ public boolean onTouch(View v, android.view.MotionEvent event) {
+ if (event.getAction() == android.view.MotionEvent.ACTION_UP || event.getAction() == android.view.MotionEvent.ACTION_CANCEL) {
+ if (isPullingToDismiss) {
+ float deltaY = event.getRawY() - startY;
+ if (deltaY > v.getHeight() * 0.20f) {
+ context.onBackPressed();
+ } else {
+ holder.parentBinding.content.animate().scaleX(1f).scaleY(1f).translationY(0).setDuration(250).start();
+ }
+ isPullingToDismiss = false;
+ return true;
+ }
+ }
+
+ if (event.getY() > (holder.binding.playerView.getHeight() * 0.75f)) {
+ return false;
+ }
+ gestureDetector.onTouchEvent(event);
+ return true;
+ }
});
}
@@ -364,7 +708,10 @@ private void playVideo(FragmentActivity context, Uri fileUri, GalleryPagerViewHo
@Override
public void onIsPlayingChanged(boolean isPlaying) {
Player.Listener.super.onIsPlayingChanged(isPlaying);
- holder.parentBinding.lLButtons.setVisibility(isPlaying ? View.INVISIBLE : View.VISIBLE);
+
+ ImageButton playBtn = holder.binding.playerView.findViewById(R.id.custom_play_pause);
+ if (playBtn != null) playBtn.setImageResource(isPlaying ? R.drawable.ic_baseline_pause_24 : R.drawable.ic_baseline_play_arrow_24);
+
if (!isPlaying) {
galleryViewModel.setVideoPosition(finalPlayer.getCurrentPosition(), fileUri);
}
@@ -379,11 +726,15 @@ public void onPlayerError(@NonNull PlaybackException error) {
holder.binding.playerView.setPlayer(player);
player.prepare();
player.setPlayWhenReady(true);
- holder.binding.playerView.hideController();
+ holder.binding.playerView.showController();
}
private void setupImageView(GalleryPagerViewHolder holder, FragmentActivity context, GalleryFile galleryFile) {
if (holder instanceof GalleryPagerViewHolder.GalleryPagerImageViewHolder) {
+
+ // --- NEW: Attach Pull to dismiss to the Image ---
+ attachPullToDismiss(((GalleryPagerViewHolder.GalleryPagerImageViewHolder) holder).binding.imageView, holder.parentBinding.content, context);
+
((GalleryPagerViewHolder.GalleryPagerImageViewHolder) holder).binding.imageView.setOnClickListener(v -> onItemPressed(context));
((GalleryPagerViewHolder.GalleryPagerImageViewHolder) holder).binding.imageView.setMinimumDpi(40);
((GalleryPagerViewHolder.GalleryPagerImageViewHolder) holder).binding.imageView.setOrientation(MySubsamplingScaleImageView.ORIENTATION_USE_EXIF);
@@ -400,6 +751,10 @@ public void onCenterChanged(PointF newCenter, int origin) {
});
loadImage(galleryFile, (GalleryPagerViewHolder.GalleryPagerImageViewHolder) holder, context);
} else if (holder instanceof GalleryPagerViewHolder.GalleryPagerGifViewHolder) {
+
+ // --- NEW: Attach Pull to dismiss to Gifs ---
+ attachPullToDismiss(((GalleryPagerViewHolder.GalleryPagerGifViewHolder) holder).binding.gifImageView, holder.parentBinding.content, context);
+
((GalleryPagerViewHolder.GalleryPagerGifViewHolder) holder).binding.gifImageView.setOnClickListener(v -> onItemPressed(context));
loadGif(galleryFile, (GalleryPagerViewHolder.GalleryPagerGifViewHolder) holder, context);
}
@@ -486,16 +841,20 @@ private int exifToDegrees(int orientation) {
private void loadGif(GalleryFile galleryFile, GalleryPagerViewHolder.GalleryPagerGifViewHolder holder, FragmentActivity context) {
Glide.with(context)
- //.asGif()
.load(galleryFile.getUri())
.apply(GlideStuff.getRequestOptions(useDiskCache))
.into(holder.binding.gifImageView);
}
+ // --- NEW: Updated showButtons to respect Palette Color ---
private void showButtons(GalleryPagerViewHolder holder, boolean show) {
if (isFullscreen) {
show = false;
- holder.parentBinding.getRoot().setBackgroundColor(weakReference.get().getResources().getColor(R.color.black, weakReference.get().getTheme()));
+ Object tag = holder.parentBinding.getRoot().getTag();
+ int defaultColor = weakReference.get().getResources().getColor(R.color.black, weakReference.get().getTheme());
+ int color = tag instanceof Integer ? (int) tag : defaultColor;
+
+ holder.parentBinding.getRoot().setBackgroundColor(color);
} else {
holder.parentBinding.getRoot().setBackgroundColor(MaterialColors.getColor(weakReference.get(), R.attr.gallery_viewpager_background, Color.WHITE));
}
@@ -538,9 +897,9 @@ private void showMenu(FragmentActivity context, GalleryFile galleryFile, Gallery
}
return true;
});
- menu.getItem(2).setVisible(!isAllFolder); // hide edit note in All folder
+ menu.getItem(2).setVisible(!isAllFolder);
menu.getItem(2).setEnabled(!isAllFolder);
- menu.getItem(3).setVisible(!isAllFolder && galleryFile.isText()); // hide edit text in All folder and for non-text files
+ menu.getItem(3).setVisible(!isAllFolder && galleryFile.isText());
menu.getItem(3).setEnabled(!isAllFolder && galleryFile.isText());
popup.show();
@@ -553,13 +912,11 @@ private void showEditNote(FragmentActivity context, GalleryFile galleryFile, Gal
}
galleryFile.setNote(text);
if (text == null) {
- // delete note
if (galleryFile.hasNote()) {
FileStuff.deleteFile(context, galleryFile.getNoteUri());
galleryFile.setNoteUri(null);
}
} else if (galleryFile.hasNote()) {
- // overwrite
deleteNote(context, galleryFile);
saveNote(context, galleryFile, text);
} else {
@@ -639,7 +996,7 @@ public void onError(Exception e) {
public void onInvalidPassword(InvalidPasswordException e) {
//removeFileAt(holder.getAdapterPosition(), context);
}
- }; // TODO does not export to current directory
+ };
Encryption.decryptAndExport(context, galleryFile.getUri(), currentDirectory, galleryFile, galleryFile.isVideo(), galleryFile.getVersion(), password.getPassword(), result);
}).start());
}
@@ -689,38 +1046,7 @@ private void saveNote(FragmentActivity context, GalleryFile galleryFile, String
}
private void loadNote(GalleryPagerViewHolder holder, FragmentActivity context, GalleryFile galleryFile) {
- if (galleryFile.hasNote()) {
- if (galleryFile.getNote() != null) {
- holder.parentBinding.noteLayout.setVisibility(View.VISIBLE);
- holder.parentBinding.note.setText(context.getString(R.string.gallery_note_click_to_show));
- final boolean[] expanded = {false};
- View.OnClickListener onClickListener = v -> {
- if (expanded[0]) {
- holder.parentBinding.noteAction.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.round_expand_less_24, context.getTheme()));
- holder.parentBinding.note.setText(context.getString(R.string.gallery_note_click_to_show));
- holder.parentBinding.noteLayout.setBackgroundColor(MaterialColors.getColor(context, R.attr.gallery_viewpager_buttons_background, Color.BLACK));
- } else {
- holder.parentBinding.noteAction.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.round_expand_more_24, context.getTheme()));
- holder.parentBinding.note.setText(galleryFile.getNote());
- holder.parentBinding.noteLayout.setBackgroundColor(MaterialColors.getColor(context, R.attr.gallery_viewpager_note_background, Color.BLACK));
- }
- expanded[0] = !expanded[0];
- };
- holder.parentBinding.noteAction.setOnClickListener(onClickListener);
- holder.parentBinding.note.setOnClickListener(onClickListener);
- } else {
- holder.parentBinding.noteLayout.setVisibility(View.VISIBLE);
- holder.parentBinding.note.setText(context.getString(R.string.gallery_loading_note));
- new Thread(() -> {
- String text = Encryption.readEncryptedTextFromUri(galleryFile.getNoteUri(), context, galleryFile.getVersion(), password.getPassword());
- galleryFile.setNote(text);
- context.runOnUiThread(() -> notifyItemChanged(holder.getBindingAdapterPosition(), new GalleryGridAdapter.Payload(GalleryGridAdapter.Payload.TYPE_LOADED_NOTE)));
- }).start();
- }
- } else {
- holder.parentBinding.noteLayout.setVisibility(View.GONE);
- holder.parentBinding.note.setText("");
- }
+ // Intentionally left blank. Note UI was removed for a cleaner edge-to-edge layout!
}
private void removeFileAt(int pos, FragmentActivity context) {
@@ -739,6 +1065,47 @@ public int getItemViewType(int position) {
return galleryFile.getFileType().type;
}
+ // --- NEW: Smart ViewPager Scroll Engine ---
+ private androidx.viewpager2.widget.ViewPager2 attachedViewPager;
+
+ @Override
+ public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+ super.onAttachedToRecyclerView(recyclerView);
+ if (recyclerView.getParent() instanceof androidx.viewpager2.widget.ViewPager2) {
+ attachedViewPager = (androidx.viewpager2.widget.ViewPager2) recyclerView.getParent();
+ attachedViewPager.registerOnPageChangeCallback(new androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback() {
+ @Override
+ public void onPageSelected(int position) {
+ super.onPageSelected(position);
+ triggerActiveVideo(position);
+ }
+ });
+ }
+ }
+
+ public void triggerActiveVideo(int position) {
+ if (position < 0 || position >= galleryFiles.size()) return;
+
+ GalleryFile file = galleryFiles.get(position);
+ if (file.isVideo() || file.isAudio()) {
+ if (attachedViewPager != null) {
+ RecyclerView rv = (RecyclerView) attachedViewPager.getChildAt(0);
+ rv.post(() -> {
+ // Force pause all background players to free up the decryption engine!
+ pausePlayers();
+
+ RecyclerView.ViewHolder holder = rv.findViewHolderForAdapterPosition(position);
+ if (holder instanceof GalleryPagerViewHolder.GalleryPagerVideoViewHolder) {
+ playVideo(weakReference.get(), file.getUri(), (GalleryPagerViewHolder.GalleryPagerVideoViewHolder) holder, file.getVersion(), galleryViewModel.getVideoPosition(file.getUri()));
+ }
+ });
+ }
+ } else {
+ // If the user swiped to an image, immediately pause the audio/video!
+ pausePlayers();
+ }
+ }
+
@Override
public void onViewDetachedFromWindow(@NonNull GalleryPagerViewHolder holder) {
if (holder instanceof GalleryPagerViewHolder.GalleryPagerVideoViewHolder vh) {
@@ -760,6 +1127,7 @@ public void onViewRecycled(@NonNull GalleryPagerViewHolder holder) {
private void releaseVideo(GalleryPagerViewHolder.GalleryPagerVideoViewHolder holder) {
final int pos = holder.getBindingAdapterPosition();
holder.binding.playerView.setPlayer(null);
+
if (pos >= 0) {
ExoPlayer player = players.remove(pos);
if (player != null) {
@@ -812,4 +1180,4 @@ public boolean videoIsLoaded(int pos) {
ExoPlayer player = players.get(pos);
return player != null && !player.isReleased();
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/se/arctosoft/vault/data/FileType.java b/app/src/main/java/se/arctosoft/vault/data/FileType.java
index b0c723a..3e9b048 100644
--- a/app/src/main/java/se/arctosoft/vault/data/FileType.java
+++ b/app/src/main/java/se/arctosoft/vault/data/FileType.java
@@ -31,13 +31,18 @@ public enum FileType {
VIDEO_V1(3, ".mp4", Encryption.PREFIX_VIDEO_FILE, 1),
VIDEO_V2(3, ".mp4", Encryption.SUFFIX_VIDEO_FILE, 2),
TEXT_V1(4, ".txt", Encryption.PREFIX_TEXT_FILE, 1),
- TEXT_V2(4, ".txt", Encryption.SUFFIX_TEXT_FILE, 2);
+ TEXT_V2(4, ".txt", Encryption.SUFFIX_TEXT_FILE, 2),
+
+ // --- NEW: Audio Types ---
+ AUDIO_V1(5, ".mp3", ".aud-", 1),
+ AUDIO_V2(5, ".mp3", "-aud", 2);
public static final int TYPE_DIRECTORY = 0;
public static final int TYPE_IMAGE = 1;
public static final int TYPE_GIF = 2;
public static final int TYPE_VIDEO = 3;
public static final int TYPE_TEXT = 4;
+ public static final int TYPE_AUDIO = 5; // The constant we needed!
public final String extension, suffixPrefix;
public final int type, version;
@@ -66,6 +71,13 @@ public static FileType fromFilename(@NonNull String name) {
return TEXT_V1;
} else if (name.endsWith(Encryption.SUFFIX_TEXT_FILE)) {
return TEXT_V2;
+
+ // --- NEW: Audio Name Parsing ---
+ } else if (name.startsWith(".aud-")) {
+ return AUDIO_V1;
+ } else if (name.endsWith("-aud")) {
+ return AUDIO_V2;
+
} else {
return DIRECTORY;
}
@@ -83,7 +95,6 @@ public boolean isGif() {
return this == GIF_V1 || this == GIF_V2;
}
-
public boolean isVideo() {
return this == VIDEO_V1 || this == VIDEO_V2;
}
@@ -91,4 +102,9 @@ public boolean isVideo() {
public boolean isText() {
return this == TEXT_V1 || this == TEXT_V2;
}
-}
+
+ // --- NEW: Audio Helper Method ---
+ public boolean isAudio() {
+ return this == AUDIO_V1 || this == AUDIO_V2;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/se/arctosoft/vault/data/GalleryFile.java b/app/src/main/java/se/arctosoft/vault/data/GalleryFile.java
index b312477..c1d0ff6 100644
--- a/app/src/main/java/se/arctosoft/vault/data/GalleryFile.java
+++ b/app/src/main/java/se/arctosoft/vault/data/GalleryFile.java
@@ -189,6 +189,10 @@ public boolean isVideo() {
return fileType.type == FileType.TYPE_VIDEO;
}
+ public boolean isAudio() {
+ return getFileType().type == FileType.TYPE_AUDIO;
+ }
+
public boolean isGif() {
return fileType.type == FileType.TYPE_GIF;
}
diff --git a/app/src/main/java/se/arctosoft/vault/utils/FileStuff.java b/app/src/main/java/se/arctosoft/vault/utils/FileStuff.java
index a33e0bf..aafc539 100644
--- a/app/src/main/java/se/arctosoft/vault/utils/FileStuff.java
+++ b/app/src/main/java/se/arctosoft/vault/utils/FileStuff.java
@@ -49,42 +49,37 @@ public class FileStuff {
@NonNull
public static List getFilesInFolder(Context context, Uri pickedDir) {
- //long start = System.currentTimeMillis();
- //Log.e(TAG, "getFilesInFolder: " + pickedDir);
Uri realUri = DocumentsContract.buildChildDocumentsUriUsingTree(pickedDir, DocumentsContract.getDocumentId(pickedDir));
List files = new ArrayList<>();
- Cursor c = context.getContentResolver().query(
+
+ // Modernized: try-with-resources automatically closes the Cursor to prevent memory leaks
+ try (Cursor c = context.getContentResolver().query(
realUri,
new String[]{DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_LAST_MODIFIED,
DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_SIZE},
- null,
- null,
- null);
- if (c == null || !c.moveToFirst()) {
- if (c != null) {
- c.close();
+ null, null, null)) {
+
+ if (c == null || !c.moveToFirst()) {
+ return new ArrayList<>();
}
- return new ArrayList<>();
+
+ do {
+ Uri uri = DocumentsContract.buildDocumentUriUsingTree(realUri, c.getString(0));
+ String name = c.getString(1);
+ long lastModified = c.getLong(2);
+ String mimeType = c.getString(3);
+ long size = c.getLong(4);
+
+ if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)
+ || name.startsWith(Encryption.ENCRYPTED_PREFIX)
+ || name.endsWith(Encryption.ENCRYPTED_SUFFIX)) {
+ files.add(new CursorFile(name, uri, lastModified, mimeType, size));
+ }
+ } while (c.moveToNext());
}
- //Log.e(TAG, "getFilesInFolder 1: " + (System.currentTimeMillis() - start));
- do {
- Uri uri = DocumentsContract.buildDocumentUriUsingTree(realUri, c.getString(0));
- String name = c.getString(1);
- long lastModified = c.getLong(2);
- String mimeType = c.getString(3);
- long size = c.getLong(4);
-
- if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)
- || name.startsWith(Encryption.ENCRYPTED_PREFIX)
- || name.endsWith(Encryption.ENCRYPTED_SUFFIX)) {
- files.add(new CursorFile(name, uri, lastModified, mimeType, size));
- }
- } while (c.moveToNext());
- c.close();
+
Collections.sort(files);
- //Log.e(TAG, "getFilesInFolder 2: " + (System.currentTimeMillis() - start));
List encryptedFilesInFolder = getEncryptedFilesInFolder(files);
- //Log.e(TAG, "getFilesInFolder 3: " + (System.currentTimeMillis() - start));
Collections.sort(encryptedFilesInFolder);
return encryptedFilesInFolder;
@@ -128,7 +123,6 @@ private static List getEncryptedFilesInFolder(@NonNull List getDocumentsFromDirectoryResult(Context context
}
for (Uri uri : uris) {
DocumentFile pickedFile = DocumentFile.fromSingleUri(context, uri);
- if (pickedFile != null && pickedFile.getType() != null && (pickedFile.getType().startsWith("image/") || pickedFile.getType().startsWith("video/")) &&
+ if (pickedFile != null && pickedFile.getType() != null && isSupportedMedia(pickedFile.getType()) &&
(!pickedFile.getName().endsWith(Encryption.ENCRYPTED_SUFFIX) || !pickedFile.getName().startsWith(Encryption.ENCRYPTED_PREFIX))) {
documentFiles.add(pickedFile);
}
@@ -199,7 +193,7 @@ public static List getDocumentsFromShareIntent(Context context, @N
}
for (Uri uri : uris) {
DocumentFile pickedFile = DocumentFile.fromSingleUri(context, uri);
- if (pickedFile != null && pickedFile.getType() != null && (pickedFile.getType().startsWith("image/") || pickedFile.getType().startsWith("video/")) &&
+ if (pickedFile != null && pickedFile.getType() != null && isSupportedMedia(pickedFile.getType()) &&
(!pickedFile.getName().endsWith(Encryption.ENCRYPTED_SUFFIX) || !pickedFile.getName().startsWith(Encryption.ENCRYPTED_PREFIX))) {
documentFiles.add(pickedFile);
}
@@ -207,6 +201,11 @@ public static List getDocumentsFromShareIntent(Context context, @N
return documentFiles;
}
+ // Modern helper to cleanly verify all our supported Vault types (Images, Videos, and AUDIO!)
+ private static boolean isSupportedMedia(String mimeType) {
+ return mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/");
+ }
+
public static boolean deleteFile(Context context, @Nullable Uri uri) {
if (uri == null) {
return true;
@@ -308,19 +307,16 @@ public static boolean moveTo(Context context, GalleryFile sourceFile, DocumentFi
}
public static boolean writeTo(Context context, Uri src, Uri dest) {
- try {
- InputStream inputStream = new BufferedInputStream(context.getContentResolver().openInputStream(src), 1024 * 32);
- OutputStream outputStream = new BufferedOutputStream(context.getContentResolver().openOutputStream(dest));
+ // Modernized: try-with-resources handles all stream closing safely automatically
+ try (InputStream inputStream = new BufferedInputStream(context.getContentResolver().openInputStream(src), 1024 * 32);
+ OutputStream outputStream = new BufferedOutputStream(context.getContentResolver().openOutputStream(dest))) {
+
int read;
- byte[] buffer = new byte[2048];
+ byte[] buffer = new byte[8192]; // Bumped up buffer size to 8KB for faster flash storage I/O
while ((read = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, read);
}
- try {
- outputStream.close();
- inputStream.close();
- } catch (IOException ignored) {
- }
+
} catch (IOException e) {
Log.e(TAG, "writeTo: failed to write: " + e.getMessage());
e.printStackTrace();
@@ -328,4 +324,4 @@ public static boolean writeTo(Context context, Uri src, Uri dest) {
}
return true;
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_floating_nav.xml b/app/src/main/res/drawable/bg_floating_nav.xml
new file mode 100644
index 0000000..2823a4c
--- /dev/null
+++ b/app/src/main/res/drawable/bg_floating_nav.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_rounded_toast.xml b/app/src/main/res/drawable/bg_rounded_toast.xml
new file mode 100644
index 0000000..4685b01
--- /dev/null
+++ b/app/src/main/res/drawable/bg_rounded_toast.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml
new file mode 100644
index 0000000..0cdf891
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_baseline_pause_24.xml b/app/src/main/res/drawable/ic_baseline_pause_24.xml
new file mode 100644
index 0000000..05825d5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_pause_24.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_outline_audio_file_24.xml b/app/src/main/res/drawable/ic_outline_audio_file_24.xml
new file mode 100644
index 0000000..6cba2b2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_audio_file_24.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/outline_music_note_24.xml b/app/src/main/res/drawable/outline_music_note_24.xml
new file mode 100644
index 0000000..7918e50
--- /dev/null
+++ b/app/src/main/res/drawable/outline_music_note_24.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/outline_picture_in_picture_alt_24.xml b/app/src/main/res/drawable/outline_picture_in_picture_alt_24.xml
new file mode 100644
index 0000000..b185dbf
--- /dev/null
+++ b/app/src/main/res/drawable/outline_picture_in_picture_alt_24.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/outline_screen_rotation_24.xml b/app/src/main/res/drawable/outline_screen_rotation_24.xml
new file mode 100644
index 0000000..9c11759
--- /dev/null
+++ b/app/src/main/res/drawable/outline_screen_rotation_24.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/outline_subtitles_24.xml b/app/src/main/res/drawable/outline_subtitles_24.xml
new file mode 100644
index 0000000..22bd91b
--- /dev/null
+++ b/app/src/main/res/drawable/outline_subtitles_24.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/adapter_gallery_grid_item.xml b/app/src/main/res/layout/adapter_gallery_grid_item.xml
index 3613669..986e0d4 100644
--- a/app/src/main/res/layout/adapter_gallery_grid_item.xml
+++ b/app/src/main/res/layout/adapter_gallery_grid_item.xml
@@ -1,83 +1,116 @@
-
+ android:layout_margin="2dp"
+ android:clipChildren="false"
+ android:clipToPadding="false">
-
+
+ android:layout_height="0dp"
+ app:cardCornerRadius="16dp"
+ app:cardElevation="0dp"
+ app:strokeWidth="0dp"
+ app:layout_constraintDimensionRatio="1:1"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent">
-
+ android:layout_height="match_parent">
+
+ android:scaleType="centerCrop"
+ android:transitionName="shared_gallery_image" />
-
+ android:layout_marginStart="2dp"
+ android:layout_marginTop="2dp"
+ android:clickable="false"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
-
+
-
+
-
+
+
-
+
+
-
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/adapter_gallery_viewpager_item.xml b/app/src/main/res/layout/adapter_gallery_viewpager_item.xml
index fe6fa12..fc31c05 100644
--- a/app/src/main/res/layout/adapter_gallery_viewpager_item.xml
+++ b/app/src/main/res/layout/adapter_gallery_viewpager_item.xml
@@ -1,155 +1,116 @@
-
+ android:layout_height="match_parent">
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:background="@android:color/transparent"
+ android:gravity="center"
+ android:orientation="vertical"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/adapter_gallery_viewpager_item_video.xml b/app/src/main/res/layout/adapter_gallery_viewpager_item_video.xml
index 99d2c98..2044512 100644
--- a/app/src/main/res/layout/adapter_gallery_viewpager_item_video.xml
+++ b/app/src/main/res/layout/adapter_gallery_viewpager_item_video.xml
@@ -1,37 +1,59 @@
-
+ android:layout_height="match_parent"
+ android:background="@android:color/black">
+ app:show_buffering="always"
+ app:controller_layout_id="@layout/custom_video_controller" />
-
+
+ android:layout_height="match_parent"
+ android:background="@android:color/black">
+ android:adjustViewBounds="true"
+ android:scaleType="fitCenter"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+
+
+ android:layout_width="80dp"
+ android:layout_height="80dp"
+ android:src="@drawable/ic_baseline_play_arrow_24"
+ app:tint="#FFFFFF"
+ android:background="?selectableItemBackgroundBorderless"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/bottom_sheet_import.xml b/app/src/main/res/layout/bottom_sheet_import.xml
index ddb1163..f70916a 100644
--- a/app/src/main/res/layout/bottom_sheet_import.xml
+++ b/app/src/main/res/layout/bottom_sheet_import.xml
@@ -3,116 +3,137 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical"
- android:padding="20dp"
+ android:fillViewport="true"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
+ android:orientation="vertical"
+ android:paddingBottom="24dp">
-
+
-
-
+ android:layout_height="wrap_content" />
+
-
-
+ android:orientation="vertical"
+ android:paddingHorizontal="24dp">
+
+ android:text="@plurals/import_modal_title"
+ android:textAppearance="?attr/textAppearanceTitleLarge"
+ android:textStyle="bold" />
-
-
-
-
-
-
-
-
-
-
+ android:layout_marginTop="8dp"
+ android:text="@string/import_modal_body"
+ android:textAppearance="?attr/textAppearanceBodyMedium"
+ android:textColor="?android:attr/textColorSecondary" />
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:layout_marginTop="24dp"
+ android:orientation="vertical"
+ android:visibility="gone">
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/custom_video_controller.xml b/app/src/main/res/layout/custom_video_controller.xml
new file mode 100644
index 0000000..07d4a56
--- /dev/null
+++ b/app/src/main/res/layout/custom_video_controller.xml
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_directory.xml b/app/src/main/res/layout/fragment_directory.xml
index 6a23665..c60f053 100644
--- a/app/src/main/res/layout/fragment_directory.xml
+++ b/app/src/main/res/layout/fragment_directory.xml
@@ -1,18 +1,22 @@
-
+
+
+ android:visibility="gone"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent" />
+
+
+
+
+
+ android:layout_height="0dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/bottom_navigation">
+
@@ -63,6 +100,7 @@
android:layout_height="0dp"
android:fillViewport="true"
android:orientation="vertical"
+ android:scrollbars="none"
app:layout_constraintBottom_toTopOf="@id/fab"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
@@ -71,86 +109,97 @@
android:id="@+id/fabs_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:gravity="bottom"
- android:orientation="vertical">
+ android:gravity="bottom|end"
+ android:orientation="vertical"
+ android:paddingBottom="12dp">
+
-
-
+
+ android:layout_height="0dp"
+ android:visibility="gone"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/bottom_navigation">
+
+ layout="@layout/loading_item"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_password.xml b/app/src/main/res/layout/fragment_password.xml
index 65ae635..b405343 100644
--- a/app/src/main/res/layout/fragment_password.xml
+++ b/app/src/main/res/layout/fragment_password.xml
@@ -1,5 +1,6 @@
-
-
-
+
+
+
+ android:layout_gravity="center_vertical"
+ android:paddingVertical="32dp">
+
-
+ android:id="@+id/logoImage"
+ android:layout_width="80dp"
+ android:layout_height="80dp"
+ android:src="@drawable/logo"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+
+ android:textAppearance="?attr/textAppearanceHeadlineMedium"
+ android:textStyle="bold"
+ app:layout_constraintTop_toBottomOf="@id/logoImage"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
-
-
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:padding="24dp">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/menu_bottom_nav.xml b/app/src/main/res/menu/menu_bottom_nav.xml
new file mode 100644
index 0000000..bc16157
--- /dev/null
+++ b/app/src/main/res/menu/menu_bottom_nav.xml
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
index c4dcf83..645d3a6 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -25,8 +25,10 @@
app:argType="string"
app:nullable="true" />
+
+
@@ -35,6 +37,7 @@
android:name="se.arctosoft.vault.DirectoryAllFragment"
android:label="@string/gallery_all"
tools:layout="@layout/fragment_directory">
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e0e7c6f..0d5c816 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,5 +1,5 @@
- Valv
+ ExValv
Encrypt file
Decrypt file
MainActivity
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 154e229..7f8274c 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -23,6 +23,12 @@
- @color/button_warning_color
- @color/grid_stroke_color_day
- @style/MySnackBarStyle
+
+ - @android:color/transparent
+ - true
+
+
+ - @android:color/transparent