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 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