diff --git a/CHANGELOG.md b/CHANGELOG.md index 5314e9911..07b808103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,97 @@ Changelog ========== +Version 2.16.0 *(2017-10-19)* +---------------------------- + + * Added sorting by path + * Added an option to show customizable extended details over fullscreen media + * Allow selecting Album cover photos from any folders + * Added a checkbox for skipping Delete confirmation dialog + +Version 2.15.2 *(2017-10-06)* +---------------------------- + + * Properly display SD card content to Android 4 users + * Fix displaying some third party media, like Bluemail attachments + * Fix media picking intents if "Show all folders content" is enabled + +Version 2.15.1 *(2017-10-01)* +---------------------------- + + * Updated commons library with minor fixes + +Version 2.15.0 *(2017-10-01)* +---------------------------- + + * Added fingerprint to hidden item protection + * Added a new List view type + * Fixed an issue with some hidden items being shown at "Show all folders content" + * Fixed typing in color hex codes manually with some keyboards + * Do not autosave rotated images in any case + * Tons of other performance, stability and UX improvements + +Version 2.14.4 *(2017-09-18)* +---------------------------- + + * Revert to the old way of loading fullscreen images to avoid issues on Android 7+ + +Version 2.14.3 *(2017-09-17)* +---------------------------- + + * Removed some error toast messages after delete, or if image loading failed + * Fixed some visual glitches at horizontal scrolling + * Disable pull-to-refresh at horizontal scrolling + * Many other smaller bugfixes and improvements + +Version 2.14.2 *(2017-09-11)* +---------------------------- + + * Fixing some glitches with fullscreen images + * Add an extra check to avoid displaying non-existing media + * Fix opening media from third party intents + +Version 2.14.1 *(2017-09-07)* +---------------------------- + + * Fixing some glitches around fullscreen view + +Version 2.14.0 *(2017-09-05)* +---------------------------- + + * Simplified the way of creating new folders + * Added a loop option to slideshows, slowed down the swipe animation + * Added an option to filter out gifs from slideshows + * Improved the quality of fullscreen images + * Properly allow excluding the root folder + +Version 2.13.4 *(2017-09-01)* +---------------------------- + + * Improved the image loading performance + * Added a switch for disabling video gestures + * Added a switch for deleting empty folders after deleting content + * Show excluded folder content at third party intent if needed + +Version 2.13.3 *(2017-08-29)* +---------------------------- + + * Fixing copy/move actions on some devices + +Version 2.13.2 *(2017-08-28)* +---------------------------- + + * Moved media type filter from Settings to the Action menu + * Allow filtering GIFs out + * Make sure we always show manually included folders + * Properly show hidden files, when open from some File Manager + +Version 2.13.1 *(2017-08-16)* +---------------------------- + + * Show a folder if its both excluded and included + * Many translation improvements + Version 2.13.0 *(2017-08-07)* ---------------------------- diff --git a/LICENSE b/LICENSE index 28c26588c..ff97af7d9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,13 +1,201 @@ -Copyright 2016 SimpleMobileTools + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - https://www.apache.org/licenses/LICENSE-2.0 + 1. Definitions. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 SimpleMobileTools + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/app/build.gradle b/app/build.gradle index 6282b5d27..d09181550 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,14 +4,14 @@ apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 25 - buildToolsVersion "25.0.2" + buildToolsVersion "25.0.3" defaultConfig { applicationId "com.simplemobiletools.gallery" minSdkVersion 16 targetSdkVersion 23 - versionCode 124 - versionName "2.13.1" + versionCode 136 + versionName "2.16.0" } signingConfigs { @@ -37,7 +37,7 @@ android { } dependencies { - compile 'com.simplemobiletools:commons:2.25.4' + compile 'com.simplemobiletools:commons:2.30.6' compile 'com.davemorrissey.labs:subsampling-scale-image-view:3.6.0' compile 'com.theartofdev.edmodo:android-image-cropper:2.4.0' compile 'com.bignerdranch.android:recyclerview-multiselect:0.2' @@ -52,7 +52,7 @@ dependencies { } buildscript { - ext.kotlin_version = '1.1.4-2' + ext.kotlin_version = '1.1.51' repositories { mavenCentral() } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 0816ea160..a9c0f371b 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,5 +1,2 @@ -keep class com.simplemobiletools.** { *; } -dontwarn com.simplemobiletools.** - --renamesourcefileattribute SourceFile --keepattributes SourceFile,LineNumberTable diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/App.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/App.kt index 4cbda3237..48ef23b19 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/App.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/App.kt @@ -1,12 +1,14 @@ package com.simplemobiletools.gallery import android.app.Application +import com.github.ajalt.reprint.core.Reprint import com.squareup.leakcanary.LeakCanary class App : Application() { val USE_LEAK_CANARY = false override fun onCreate() { super.onCreate() + Reprint.initialize(this) if (USE_LEAK_CANARY) { if (LeakCanary.isInAnalyzerProcess(this)) { return diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/activities/EditActivity.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/activities/EditActivity.kt index ca9cf9bf6..0da6c4bef 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/activities/EditActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/activities/EditActivity.kt @@ -172,7 +172,11 @@ class EditActivity : SimpleActivity(), CropImageView.OnCropImageCompleteListener try { getFileOutputStream(file) { - saveBitmap(file, bitmap, it) + if (it != null) { + saveBitmap(file, bitmap, it) + } else { + toast(R.string.image_editing_failed) + } } } catch (e: Exception) { Log.e(TAG, "Crop compressing failed $path $e") diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/activities/MainActivity.kt index 6952c945e..8baa4fa20 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/activities/MainActivity.kt @@ -1,23 +1,27 @@ package com.simplemobiletools.gallery.activities -import android.Manifest import android.app.Activity +import android.content.ClipData import android.content.Intent -import android.content.pm.PackageManager import android.net.Uri -import android.os.AsyncTask import android.os.Build import android.os.Bundle import android.os.Handler import android.provider.MediaStore -import android.support.v4.app.ActivityCompat import android.support.v7.widget.GridLayoutManager import android.view.Menu import android.view.MenuItem import android.view.ViewGroup import android.widget.FrameLayout import com.google.gson.Gson +import com.simplemobiletools.commons.dialogs.CreateNewFolderDialog +import com.simplemobiletools.commons.dialogs.FilePickerDialog +import com.simplemobiletools.commons.dialogs.RadioGroupDialog import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.helpers.PERMISSION_WRITE_STORAGE +import com.simplemobiletools.commons.helpers.SORT_BY_DATE_MODIFIED +import com.simplemobiletools.commons.helpers.SORT_BY_DATE_TAKEN +import com.simplemobiletools.commons.models.RadioItem import com.simplemobiletools.commons.models.Release import com.simplemobiletools.commons.views.MyScalableRecyclerView import com.simplemobiletools.gallery.BuildConfig @@ -25,6 +29,7 @@ import com.simplemobiletools.gallery.R import com.simplemobiletools.gallery.adapters.DirectoryAdapter import com.simplemobiletools.gallery.asynctasks.GetDirectoriesAsynctask import com.simplemobiletools.gallery.dialogs.ChangeSortingDialog +import com.simplemobiletools.gallery.dialogs.FilterMediaDialog import com.simplemobiletools.gallery.extensions.* import com.simplemobiletools.gallery.helpers.* import com.simplemobiletools.gallery.models.Directory @@ -33,7 +38,6 @@ import java.io.* import java.util.* class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { - private val STORAGE_PERMISSION = 1 private val PICK_MEDIA = 2 private val PICK_WALLPAPER = 3 private val LAST_MEDIA_CHECK_PERIOD = 3000L @@ -46,20 +50,22 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { private var mIsGetVideoContentIntent = false private var mIsGetAnyContentIntent = false private var mIsSetWallpaperIntent = false + private var mAllowPickingMultiple = false private var mIsThirdPartyIntent = false private var mIsGettingDirs = false private var mStoredAnimateGifs = true private var mStoredCropThumbnails = true private var mStoredScrollHorizontally = true + private var mStoredTextColor = 0 private var mLoadedInitialPhotos = false - private var mLastMediaModified = 0 + private var mLatestMediaId = 0L private var mLastMediaHandler = Handler() - private var mCurrAsyncTask: GetDirectoriesAsynctask? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + storeStoragePaths() mIsPickImageIntent = isPickImageIntent(intent) mIsPickVideoIntent = isPickVideoIntent(intent) @@ -67,16 +73,22 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { mIsGetVideoContentIntent = isGetVideoContentIntent(intent) mIsGetAnyContentIntent = isGetAnyContentIntent(intent) mIsSetWallpaperIntent = isSetWallpaperIntent(intent) + mAllowPickingMultiple = intent.getBooleanExtra(Intent.EXTRA_ALLOW_MULTIPLE, false) mIsThirdPartyIntent = mIsPickImageIntent || mIsPickVideoIntent || mIsGetImageContentIntent || mIsGetVideoContentIntent || mIsGetAnyContentIntent || mIsSetWallpaperIntent + removeTempFolder() directories_refresh_layout.setOnRefreshListener({ getDirectories() }) - mDirs = ArrayList() + mDirs = ArrayList() mStoredAnimateGifs = config.animateGifs mStoredCropThumbnails = config.cropThumbnails mStoredScrollHorizontally = config.scrollHorizontally - storeStoragePaths() + mStoredTextColor = config.textColor checkWhatsNewDialog() + + directories_empty_text.setOnClickListener { + showFilterMediaDialog() + } } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -84,8 +96,8 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { menuInflater.inflate(R.menu.menu_main_intent, menu) } else { menuInflater.inflate(R.menu.menu_main, menu) - menu.findItem(R.id.increase_column_count).isVisible = config.dirColumnCnt < 10 - menu.findItem(R.id.reduce_column_count).isVisible = config.dirColumnCnt > 1 + menu.findItem(R.id.increase_column_count).isVisible = config.viewTypeFolders == VIEW_TYPE_GRID && config.dirColumnCnt < 10 + menu.findItem(R.id.reduce_column_count).isVisible = config.viewTypeFolders == VIEW_TYPE_GRID && config.dirColumnCnt > 1 } menu.findItem(R.id.temporarily_show_hidden).isVisible = !config.shouldShowHidden menu.findItem(R.id.stop_showing_hidden).isVisible = config.temporarilyShowHidden @@ -95,10 +107,13 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.sort -> showSortingDialog() + R.id.filter -> showFilterMediaDialog() R.id.open_camera -> launchCamera() R.id.show_all -> showAllMedia() + R.id.change_view_type -> changeViewType() R.id.temporarily_show_hidden -> tryToggleTemporarilyShowHidden() R.id.stop_showing_hidden -> tryToggleTemporarilyShowHidden() + R.id.create_new_folder -> createNewFolder() R.id.increase_column_count -> increaseColumnCount() R.id.reduce_column_count -> reduceColumnCount() R.id.settings -> launchSettings() @@ -110,6 +125,7 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { override fun onResume() { super.onResume() + config.isThirdPartyIntent = false if (mStoredAnimateGifs != config.animateGifs) { directories_grid.adapter?.notifyDataSetChanged() } @@ -119,54 +135,66 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { } if (mStoredScrollHorizontally != config.scrollHorizontally) { - directories_grid.adapter?.let { - (it as DirectoryAdapter).scrollVertically = !config.scrollHorizontally - it.notifyDataSetChanged() + (directories_grid.adapter as? DirectoryAdapter)?.apply { + scrollVertically = config.viewTypeFolders == VIEW_TYPE_LIST || !config.scrollHorizontally + notifyDataSetChanged() } setupScrollDirection() } + if (mStoredTextColor != config.textColor) { + (directories_grid.adapter as? DirectoryAdapter)?.updateTextColor(config.textColor) + } + tryloadGallery() invalidateOptionsMenu() + directories_empty_text_label.setTextColor(config.textColor) + directories_empty_text.setTextColor(config.primaryColor) } override fun onPause() { super.onPause() - mCurrAsyncTask?.shouldStop = true storeDirectories() directories_refresh_layout.isRefreshing = false mIsGettingDirs = false mStoredAnimateGifs = config.animateGifs mStoredCropThumbnails = config.cropThumbnails mStoredScrollHorizontally = config.scrollHorizontally + mStoredTextColor = config.textColor directories_grid.listener = null mLastMediaHandler.removeCallbacksAndMessages(null) + + if (!mDirs.isEmpty()) { + mCurrAsyncTask?.stopFetching() + } } override fun onDestroy() { super.onDestroy() config.temporarilyShowHidden = false + removeTempFolder() + } + + private fun removeTempFolder() { + val newFolder = File(config.tempFolderPath) + if (newFolder.exists() && newFolder.isDirectory) { + if (newFolder.list()?.isEmpty() == true) { + deleteFileBg(newFolder, true) { } + } + } + config.tempFolderPath = "" } private fun tryloadGallery() { - if (hasWriteStoragePermission()) { - if (config.showAll) - showAllMedia() - else - getDirectories() - setupLayoutManager() - checkIfColorChanged() - } else { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), STORAGE_PERMISSION) - } - } + handlePermission(PERMISSION_WRITE_STORAGE) { + if (it) { + if (config.showAll) + showAllMedia() + else + getDirectories() - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - - if (requestCode == STORAGE_PERMISSION) { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - getDirectories() + setupLayoutManager() + checkIfColorChanged() } else { toast(R.string.no_storage_permissions) finish() @@ -181,7 +209,7 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { mIsGettingDirs = true val dirs = getCachedDirectories() if (dirs.isNotEmpty() && !mLoadedInitialPhotos) { - gotDirectories(dirs) + gotDirectories(dirs, true) } if (!mLoadedInitialPhotos) { @@ -190,13 +218,24 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { mLoadedInitialPhotos = true mCurrAsyncTask = GetDirectoriesAsynctask(applicationContext, mIsPickVideoIntent || mIsGetVideoContentIntent, mIsPickImageIntent || mIsGetImageContentIntent) { - gotDirectories(it) + gotDirectories(addTempFolderIfNeeded(it), false) } - mCurrAsyncTask!!.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) + mCurrAsyncTask!!.execute() } private fun showSortingDialog() { ChangeSortingDialog(this, true, false) { + if (config.directorySorting and SORT_BY_DATE_MODIFIED > 0 || config.directorySorting and SORT_BY_DATE_TAKEN > 0) { + getDirectories() + } else { + gotDirectories(mDirs, true) + } + } + } + + private fun showFilterMediaDialog() { + FilterMediaDialog(this) { + directories_refresh_layout.isRefreshing = true getDirectories() } } @@ -205,9 +244,28 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { config.showAll = true Intent(this, MediaActivity::class.java).apply { putExtra(DIRECTORY, "/") - startActivity(this) + + if (mIsThirdPartyIntent) { + handleMediaIntent(this) + } else { + startActivity(this) + finish() + } + } + } + + private fun changeViewType() { + val items = arrayListOf( + RadioItem(VIEW_TYPE_GRID, getString(R.string.grid)), + RadioItem(VIEW_TYPE_LIST, getString(R.string.list))) + + RadioGroupDialog(this, items, config.viewTypeFolders) { + config.viewTypeFolders = it as Int + invalidateOptionsMenu() + setupLayoutManager() + directories_grid.adapter = null + setupAdapter() } - finish() } private fun tryToggleTemporarilyShowHidden() { @@ -247,6 +305,13 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { private fun getRecyclerAdapter() = (directories_grid.adapter as DirectoryAdapter) private fun setupLayoutManager() { + if (config.viewTypeFolders == VIEW_TYPE_GRID) + setupGridLayoutManager() + else + setupListLayoutManager() + } + + private fun setupGridLayoutManager() { val layoutManager = directories_grid.layoutManager as GridLayoutManager if (config.scrollHorizontally) { layoutManager.orientation = GridLayoutManager.HORIZONTAL @@ -284,6 +349,25 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { } } + private fun setupListLayoutManager() { + directories_grid.isDragSelectionEnabled = true + directories_grid.isZoomingEnabled = false + + val layoutManager = directories_grid.layoutManager as GridLayoutManager + layoutManager.spanCount = 1 + layoutManager.orientation = GridLayoutManager.VERTICAL + directories_refresh_layout.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + private fun createNewFolder() { + FilePickerDialog(this, internalStoragePath, false, config.shouldShowHidden) { + CreateNewFolderDialog(this, it) { + config.tempFolderPath = it + gotDirectories(addTempFolderIfNeeded(mDirs), true) + } + } + } + private fun increaseColumnCount() { config.dirColumnCnt = ++(directories_grid.layoutManager as GridLayoutManager).spanCount invalidateOptionsMenu() @@ -326,36 +410,22 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { if (resultCode == Activity.RESULT_OK) { - if (requestCode == PICK_MEDIA && resultData?.data != null) { - Intent().apply { + if (requestCode == PICK_MEDIA && resultData != null) { + val resultIntent = Intent() + if (mIsGetImageContentIntent || mIsGetVideoContentIntent || mIsGetAnyContentIntent) { + when { + intent.extras?.containsKey(MediaStore.EXTRA_OUTPUT) == true -> fillExtraOutput(resultData) + resultData.extras?.containsKey(PICKED_PATHS) == true -> fillPickedPaths(resultData, resultIntent) + else -> fillIntentPath(resultData, resultIntent) + } + } else if ((mIsPickImageIntent || mIsPickVideoIntent)) { val path = resultData.data.path val uri = Uri.fromFile(File(path)) - if (mIsGetImageContentIntent || mIsGetVideoContentIntent || mIsGetAnyContentIntent) { - if (intent.extras?.containsKey(MediaStore.EXTRA_OUTPUT) == true) { - var inputStream: InputStream? = null - var outputStream: OutputStream? = null - try { - val output = intent.extras.get(MediaStore.EXTRA_OUTPUT) as Uri - inputStream = FileInputStream(File(path)) - outputStream = contentResolver.openOutputStream(output) - inputStream.copyTo(outputStream) - } catch (ignored: FileNotFoundException) { - } finally { - inputStream?.close() - outputStream?.close() - } - } else { - val type = File(path).getMimeType("image/jpeg") - setDataAndTypeAndNormalize(uri, type) - flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - } - } else if (mIsPickImageIntent || mIsPickVideoIntent) { - data = uri - flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - } - - setResult(Activity.RESULT_OK, this) + resultIntent.data = uri + resultIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION } + + setResult(Activity.RESULT_OK, resultIntent) finish() } else if (requestCode == PICK_WALLPAPER) { setResult(Activity.RESULT_OK) @@ -365,10 +435,51 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { super.onActivityResult(requestCode, resultCode, resultData) } + private fun fillExtraOutput(resultData: Intent) { + val path = resultData.data.path + var inputStream: InputStream? = null + var outputStream: OutputStream? = null + try { + val output = intent.extras.get(MediaStore.EXTRA_OUTPUT) as Uri + inputStream = FileInputStream(File(path)) + outputStream = contentResolver.openOutputStream(output) + inputStream.copyTo(outputStream) + } catch (ignored: FileNotFoundException) { + } finally { + inputStream?.close() + outputStream?.close() + } + } + + private fun fillPickedPaths(resultData: Intent, resultIntent: Intent) { + val paths = resultData.extras.getStringArrayList(PICKED_PATHS) + val uris = paths.map { Uri.fromFile(File(it)) } as ArrayList + val clipData = ClipData("Attachment", arrayOf("image/*", "video/*"), ClipData.Item(uris.removeAt(0))) + + uris.forEach { + clipData.addItem(ClipData.Item(it)) + } + + resultIntent.clipData = clipData + } + + private fun fillIntentPath(resultData: Intent, resultIntent: Intent) { + val path = resultData.data.path + val uri = Uri.fromFile(File(path)) + val type = File(path).getMimeType("image/jpeg") + resultIntent.setDataAndTypeAndNormalize(uri, type) + resultIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + } + private fun itemClicked(path: String) { Intent(this, MediaActivity::class.java).apply { putExtra(DIRECTORY, path) + handleMediaIntent(this) + } + } + private fun handleMediaIntent(intent: Intent) { + intent.apply { if (mIsSetWallpaperIntent) { putExtra(SET_WALLPAPER_INTENT, true) startActivityForResult(this, PICK_WALLPAPER) @@ -376,30 +487,38 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { putExtra(GET_IMAGE_INTENT, mIsPickImageIntent || mIsGetImageContentIntent) putExtra(GET_VIDEO_INTENT, mIsPickVideoIntent || mIsGetVideoContentIntent) putExtra(GET_ANY_INTENT, mIsGetAnyContentIntent) + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, mAllowPickingMultiple) startActivityForResult(this, PICK_MEDIA) } } } - private fun gotDirectories(dirs: ArrayList) { - mLastMediaModified = getLastMediaModified() + private fun gotDirectories(newDirs: ArrayList, isFromCache: Boolean) { + val dirs = getSortedDirectories(newDirs) + + mLatestMediaId = getLatestMediaId() directories_refresh_layout.isRefreshing = false mIsGettingDirs = false + directories_empty_text_label.beVisibleIf(dirs.isEmpty() && !isFromCache) + directories_empty_text.beVisibleIf(dirs.isEmpty() && !isFromCache) + checkLastMediaChanged() - if (dirs.hashCode() == mDirs.hashCode()) + if (dirs.hashCode() == mDirs.hashCode()) { return + } mDirs = dirs runOnUiThread { setupAdapter() } + storeDirectories() } private fun storeDirectories() { - if (!config.temporarilyShowHidden) { + if (!config.temporarilyShowHidden && config.tempFolderPath.isEmpty()) { val directories = Gson().toJson(mDirs) config.directories = directories } @@ -418,13 +537,16 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { } private fun setupScrollDirection() { + val allowHorizontalScroll = config.scrollHorizontally && config.viewTypeFolders == VIEW_TYPE_GRID + directories_refresh_layout.isEnabled = !config.scrollHorizontally + directories_vertical_fastscroller.isHorizontal = false - directories_vertical_fastscroller.beGoneIf(config.scrollHorizontally) + directories_vertical_fastscroller.beGoneIf(allowHorizontalScroll) directories_horizontal_fastscroller.isHorizontal = true - directories_horizontal_fastscroller.beVisibleIf(config.scrollHorizontally) + directories_horizontal_fastscroller.beVisibleIf(allowHorizontalScroll) - if (config.scrollHorizontally) { + if (allowHorizontalScroll) { directories_horizontal_fastscroller.setViews(directories_grid, directories_refresh_layout) } else { directories_vertical_fastscroller.setViews(directories_grid, directories_refresh_layout) @@ -438,9 +560,9 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { mLastMediaHandler.removeCallbacksAndMessages(null) mLastMediaHandler.postDelayed({ Thread({ - val lastModified = getLastMediaModified() - if (mLastMediaModified != lastModified) { - mLastMediaModified = lastModified + val mediaId = getLatestMediaId() + if (mLatestMediaId != mediaId) { + mLatestMediaId = mediaId runOnUiThread { getDirectories() } @@ -459,6 +581,10 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { directories_grid.setDragSelectActive(position) } + override fun recheckPinnedFolders() { + gotDirectories(movePinnedDirectoriesToFront(mDirs), true) + } + private fun checkWhatsNewDialog() { arrayListOf().apply { add(Release(46, R.string.release_46)) @@ -494,6 +620,10 @@ class MainActivity : SimpleActivity(), DirectoryAdapter.DirOperationsListener { add(Release(119, R.string.release_119)) add(Release(122, R.string.release_122)) add(Release(123, R.string.release_123)) + add(Release(125, R.string.release_125)) + add(Release(127, R.string.release_127)) + add(Release(133, R.string.release_133)) + add(Release(136, R.string.release_136)) checkWhatsNew(this, BuildConfig.VERSION_CODE) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/activities/MediaActivity.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/activities/MediaActivity.kt index 6229ffce7..f8e79448f 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/activities/MediaActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/activities/MediaActivity.kt @@ -18,15 +18,18 @@ import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.SimpleTarget import com.bumptech.glide.request.transition.Transition import com.google.gson.Gson -import com.google.gson.reflect.TypeToken import com.simplemobiletools.commons.dialogs.ConfirmationDialog +import com.simplemobiletools.commons.dialogs.RadioGroupDialog import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.helpers.PERMISSION_WRITE_STORAGE +import com.simplemobiletools.commons.models.RadioItem import com.simplemobiletools.commons.views.MyScalableRecyclerView import com.simplemobiletools.gallery.R import com.simplemobiletools.gallery.adapters.MediaAdapter import com.simplemobiletools.gallery.asynctasks.GetMediaAsynctask import com.simplemobiletools.gallery.dialogs.ChangeSortingDialog import com.simplemobiletools.gallery.dialogs.ExcludeFolderDialog +import com.simplemobiletools.gallery.dialogs.FilterMediaDialog import com.simplemobiletools.gallery.extensions.* import com.simplemobiletools.gallery.helpers.* import com.simplemobiletools.gallery.models.Medium @@ -35,8 +38,7 @@ import java.io.File import java.io.IOException class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { - private val TAG = MediaActivity::class.java.simpleName - private val SAVE_MEDIA_CNT = 40 + private val SAVE_MEDIA_CNT = 100 private val LAST_MEDIA_CHECK_PERIOD = 3000L private var mPath = "" @@ -44,14 +46,17 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { private var mIsGetVideoIntent = false private var mIsGetAnyIntent = false private var mIsGettingMedia = false + private var mAllowPickingMultiple = false private var mShowAll = false private var mLoadedInitialPhotos = false private var mStoredAnimateGifs = true private var mStoredCropThumbnails = true private var mStoredScrollHorizontally = true + private var mStoredTextColor = 0 private var mLastDrawnHashCode = 0 - private var mLastMediaModified = 0 + private var mLatestMediaId = 0L private var mLastMediaHandler = Handler() + private var mCurrAsyncTask: GetMediaAsynctask? = null companion object { var mMedia = ArrayList() @@ -64,6 +69,7 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { mIsGetImageIntent = getBooleanExtra(GET_IMAGE_INTENT, false) mIsGetVideoIntent = getBooleanExtra(GET_VIDEO_INTENT, false) mIsGetAnyIntent = getBooleanExtra(GET_ANY_INTENT, false) + mAllowPickingMultiple = getBooleanExtra(Intent.EXTRA_ALLOW_MULTIPLE, false) } media_refresh_layout.setOnRefreshListener({ getMedia() }) @@ -71,9 +77,14 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { mStoredAnimateGifs = config.animateGifs mStoredCropThumbnails = config.cropThumbnails mStoredScrollHorizontally = config.scrollHorizontally + mStoredTextColor = config.textColor mShowAll = config.showAll if (mShowAll) supportActionBar?.setDisplayHomeAsUpEnabled(false) + + media_empty_text.setOnClickListener { + showFilterMediaDialog() + } } override fun onResume() { @@ -87,15 +98,21 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { } if (mStoredScrollHorizontally != config.scrollHorizontally) { - media_grid.adapter?.let { - (it as MediaAdapter).scrollVertically = !config.scrollHorizontally - it.notifyDataSetChanged() + (media_grid.adapter as? MediaAdapter)?.apply { + scrollVertically = config.viewTypeFiles == VIEW_TYPE_LIST || !config.scrollHorizontally + notifyDataSetChanged() } setupScrollDirection() } + if (mStoredTextColor != config.textColor) { + (media_grid.adapter as? MediaAdapter)?.updateTextColor(config.textColor) + } + tryloadGallery() invalidateOptionsMenu() + media_empty_text_label.setTextColor(config.textColor) + media_empty_text.setTextColor(config.primaryColor) } override fun onPause() { @@ -105,8 +122,13 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { mStoredAnimateGifs = config.animateGifs mStoredCropThumbnails = config.cropThumbnails mStoredScrollHorizontally = config.scrollHorizontally + mStoredTextColor = config.textColor media_grid.listener = null mLastMediaHandler.removeCallbacksAndMessages(null) + + if (!mMedia.isEmpty()) { + mCurrAsyncTask?.stopFetching() + } } override fun onDestroy() { @@ -117,14 +139,17 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { } private fun tryloadGallery() { - if (hasWriteStoragePermission()) { - val dirName = getHumanizedFilename(mPath) - title = if (mShowAll) resources.getString(R.string.all_folders) else dirName - getMedia() - setupLayoutManager() - checkIfColorChanged() - } else { - finish() + handlePermission(PERMISSION_WRITE_STORAGE) { + if (it) { + val dirName = getHumanizedFilename(mPath) + title = if (mShowAll) resources.getString(R.string.all_folders) else dirName + getMedia() + setupLayoutManager() + checkIfColorChanged() + } else { + toast(R.string.no_storage_permissions) + finish() + } } } @@ -142,7 +167,7 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { val currAdapter = media_grid.adapter if (currAdapter == null) { - media_grid.adapter = MediaAdapter(this, mMedia, this, mIsGetAnyIntent) { + media_grid.adapter = MediaAdapter(this, mMedia, this, mIsGetAnyIntent, mAllowPickingMultiple) { itemClicked(it.path) } } else { @@ -152,13 +177,16 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { } private fun setupScrollDirection() { + val allowHorizontalScroll = config.scrollHorizontally && config.viewTypeFiles == VIEW_TYPE_GRID + media_refresh_layout.isEnabled = !config.scrollHorizontally + media_vertical_fastscroller.isHorizontal = false - media_vertical_fastscroller.beGoneIf(config.scrollHorizontally) + media_vertical_fastscroller.beGoneIf(allowHorizontalScroll) media_horizontal_fastscroller.isHorizontal = true - media_horizontal_fastscroller.beVisibleIf(config.scrollHorizontally) + media_horizontal_fastscroller.beVisibleIf(allowHorizontalScroll) - if (config.scrollHorizontally) { + if (allowHorizontalScroll) { media_horizontal_fastscroller.setViews(media_grid, media_refresh_layout) } else { media_vertical_fastscroller.setViews(media_grid, media_refresh_layout) @@ -172,9 +200,9 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { mLastMediaHandler.removeCallbacksAndMessages(null) mLastMediaHandler.postDelayed({ Thread({ - val lastModified = getLastMediaModified() - if (mLastMediaModified != lastModified) { - mLastMediaModified = lastModified + val mediaId = getLatestMediaId() + if (mLatestMediaId != mediaId) { + mLatestMediaId = mediaId runOnUiThread { getMedia() } @@ -200,8 +228,10 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { findItem(R.id.temporarily_show_hidden).isVisible = !config.shouldShowHidden findItem(R.id.stop_showing_hidden).isVisible = config.temporarilyShowHidden - findItem(R.id.increase_column_count).isVisible = config.mediaColumnCnt < 10 - findItem(R.id.reduce_column_count).isVisible = config.mediaColumnCnt > 1 + findItem(R.id.increase_column_count).isVisible = config.viewTypeFiles == VIEW_TYPE_GRID && config.mediaColumnCnt < 10 + findItem(R.id.reduce_column_count).isVisible = config.viewTypeFiles == VIEW_TYPE_GRID && config.mediaColumnCnt > 1 + + findItem(R.id.toggle_filename).isVisible = config.viewTypeFiles == VIEW_TYPE_GRID } return true @@ -210,9 +240,11 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.sort -> showSortingDialog() + R.id.filter -> showFilterMediaDialog() R.id.toggle_filename -> toggleFilenameVisibility() R.id.open_camera -> launchCamera() R.id.folder_view -> switchToFolderView() + R.id.change_view_type -> changeViewType() R.id.hide_folder -> tryHideFolder() R.id.unhide_folder -> unhideFolder() R.id.exclude_folder -> tryExcludeFolder() @@ -227,24 +259,45 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { return true } - private fun toggleFilenameVisibility() { - config.displayFileNames = !config.displayFileNames - if (media_grid.adapter != null) - getRecyclerAdapter().updateDisplayFilenames(config.displayFileNames) - } - private fun showSortingDialog() { ChangeSortingDialog(this, false, !config.showAll, mPath) { getMedia() } } + private fun showFilterMediaDialog() { + FilterMediaDialog(this) { + media_refresh_layout.isRefreshing = true + getMedia() + } + } + + private fun toggleFilenameVisibility() { + config.displayFileNames = !config.displayFileNames + if (media_grid.adapter != null) + getRecyclerAdapter().updateDisplayFilenames(config.displayFileNames) + } + private fun switchToFolderView() { config.showAll = false startActivity(Intent(this, MainActivity::class.java)) finish() } + private fun changeViewType() { + val items = arrayListOf( + RadioItem(VIEW_TYPE_GRID, getString(R.string.grid)), + RadioItem(VIEW_TYPE_LIST, getString(R.string.list))) + + RadioGroupDialog(this, items, config.viewTypeFiles) { + config.viewTypeFiles = it as Int + invalidateOptionsMenu() + setupLayoutManager() + media_grid.adapter = null + setupAdapter() + } + } + private fun tryHideFolder() { if (config.wasHideFolderTooltipShown) { hideFolder() @@ -283,8 +336,8 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { private fun deleteDirectoryIfEmpty() { val file = File(mPath) - if (!file.isDownloadsFolder() && file.isDirectory && file.listFiles()?.isEmpty() == true) { - file.delete() + if (config.deleteEmptyFolders && !file.isDownloadsFolder() && file.isDirectory && file.listFiles()?.isEmpty() == true) { + deleteFile(file, true) {} } } @@ -293,22 +346,22 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { return mIsGettingMedia = true - val token = object : TypeToken>() {}.type - val media = Gson().fromJson>(config.loadFolderMedia(mPath), token) ?: ArrayList(1) + val media = getCachedMedia(mPath) if (media.isNotEmpty() && !mLoadedInitialPhotos) { - gotMedia(media) + gotMedia(media, true) } else { media_refresh_layout.isRefreshing = true } mLoadedInitialPhotos = true - GetMediaAsynctask(applicationContext, mPath, mIsGetVideoIntent, mIsGetImageIntent, mShowAll) { + mCurrAsyncTask = GetMediaAsynctask(applicationContext, mPath, mIsGetVideoIntent, mIsGetImageIntent, mShowAll) { gotMedia(it) - }.execute() + } + mCurrAsyncTask!!.execute() } private fun isDirEmpty(): Boolean { - return if (mMedia.size <= 0) { + return if (mMedia.size <= 0 && config.filterMedia > 0) { deleteDirectoryIfEmpty() finish() true @@ -335,6 +388,13 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { private fun getRecyclerAdapter() = (media_grid.adapter as MediaAdapter) private fun setupLayoutManager() { + if (config.viewTypeFiles == VIEW_TYPE_GRID) + setupGridLayoutManager() + else + setupListLayoutManager() + } + + private fun setupGridLayoutManager() { val layoutManager = media_grid.layoutManager as GridLayoutManager if (config.scrollHorizontally) { layoutManager.orientation = GridLayoutManager.HORIZONTAL @@ -372,6 +432,16 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { } } + private fun setupListLayoutManager() { + media_grid.isDragSelectionEnabled = true + media_grid.isZoomingEnabled = false + + val layoutManager = media_grid.layoutManager as GridLayoutManager + layoutManager.spanCount = 1 + layoutManager.orientation = GridLayoutManager.VERTICAL + media_refresh_layout.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + private fun increaseColumnCount() { config.mediaColumnCnt = ++(media_grid.layoutManager as GridLayoutManager).spanCount invalidateOptionsMenu() @@ -445,11 +515,14 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { } } - private fun gotMedia(media: ArrayList) { - mLastMediaModified = getLastMediaModified() + private fun gotMedia(media: ArrayList, isFromCache: Boolean = false) { + mLatestMediaId = getLatestMediaId() mIsGettingMedia = false media_refresh_layout.isRefreshing = false + media_empty_text_label.beVisibleIf(media.isEmpty() && !isFromCache) + media_empty_text.beVisibleIf(media.isEmpty() && !isFromCache) + checkLastMediaChanged() if (mLastDrawnHashCode == 0) mLastDrawnHashCode = media.hashCode() @@ -479,6 +552,7 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { if (!it) { toast(R.string.unknown_error_occurred) } else if (mMedia.isEmpty()) { + deleteDirectoryIfEmpty() finish() } } @@ -494,4 +568,12 @@ class MediaActivity : SimpleActivity(), MediaAdapter.MediaOperationsListener { override fun itemLongClicked(position: Int) { media_grid.setDragSelectActive(position) } + + override fun selectedPaths(paths: ArrayList) { + Intent().apply { + putExtra(PICKED_PATHS, paths) + setResult(Activity.RESULT_OK, this) + } + finish() + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/activities/PhotoVideoActivity.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/activities/PhotoVideoActivity.kt index 96fc44b10..eb6c13fb2 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/activities/PhotoVideoActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/activities/PhotoVideoActivity.kt @@ -1,26 +1,24 @@ package com.simplemobiletools.gallery.activities -import android.Manifest import android.content.Intent -import android.content.pm.PackageManager import android.database.Cursor import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.net.Uri import android.os.Bundle import android.provider.MediaStore -import android.support.v4.app.ActivityCompat import android.view.Menu import android.view.MenuItem import android.view.View -import com.simplemobiletools.commons.extensions.hasWriteStoragePermission import com.simplemobiletools.commons.extensions.scanPath import com.simplemobiletools.commons.extensions.toast +import com.simplemobiletools.commons.helpers.PERMISSION_WRITE_STORAGE import com.simplemobiletools.gallery.R import com.simplemobiletools.gallery.extensions.* import com.simplemobiletools.gallery.fragments.PhotoFragment import com.simplemobiletools.gallery.fragments.VideoFragment import com.simplemobiletools.gallery.fragments.ViewPagerFragment +import com.simplemobiletools.gallery.helpers.IS_FROM_GALLERY import com.simplemobiletools.gallery.helpers.IS_VIEW_INTENT import com.simplemobiletools.gallery.helpers.MEDIUM import com.simplemobiletools.gallery.models.Medium @@ -28,9 +26,9 @@ import kotlinx.android.synthetic.main.fragment_holder.* import java.io.File open class PhotoVideoActivity : SimpleActivity(), ViewPagerFragment.FragmentListener { - private val STORAGE_PERMISSION = 1 private var mMedium: Medium? = null private var mIsFullScreen = false + private var mIsFromGallery = false private var mFragment: ViewPagerFragment? = null lateinit var mUri: Uri @@ -42,16 +40,19 @@ open class PhotoVideoActivity : SimpleActivity(), ViewPagerFragment.FragmentList public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.fragment_holder) - - if (hasWriteStoragePermission()) { - checkIntent(savedInstanceState) - } else { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), STORAGE_PERMISSION) + handlePermission(PERMISSION_WRITE_STORAGE) { + if (it) { + checkIntent(savedInstanceState) + } else { + toast(R.string.no_storage_permissions) + finish() + } } } private fun checkIntent(savedInstanceState: Bundle? = null) { mUri = intent.data ?: return + mIsFromGallery = intent.getBooleanExtra(IS_FROM_GALLERY, false) if (mUri.scheme == "file") { scanPath(mUri.path) {} @@ -60,11 +61,13 @@ open class PhotoVideoActivity : SimpleActivity(), ViewPagerFragment.FragmentList return } else { val path = applicationContext.getRealPathFromURI(mUri) ?: "" - scanPath(mUri.path) {} - if (path.isNotEmpty()) { - sendViewPagerIntent(path) - finish() - return + if (path != mUri.toString()) { + scanPath(mUri.path) {} + if (path.isNotEmpty()) { + sendViewPagerIntent(path) + finish() + return + } } } @@ -110,22 +113,10 @@ open class PhotoVideoActivity : SimpleActivity(), ViewPagerFragment.FragmentList supportActionBar?.setBackgroundDrawable(resources.getDrawable(R.drawable.actionbar_gradient_background)) } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - - if (requestCode == STORAGE_PERMISSION) { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - checkIntent() - } else { - toast(R.string.no_storage_permissions) - finish() - } - } - } - private fun sendViewPagerIntent(path: String) { Intent(this, ViewPagerActivity::class.java).apply { putExtra(IS_VIEW_INTENT, true) + putExtra(IS_FROM_GALLERY, mIsFromGallery) putExtra(MEDIUM, path) startActivity(this) } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/activities/SettingsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/activities/SettingsActivity.kt index b5125dff1..8851b6bd3 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/activities/SettingsActivity.kt @@ -3,15 +3,21 @@ package com.simplemobiletools.gallery.activities import android.content.Intent import android.content.res.Resources import android.os.Bundle +import com.simplemobiletools.commons.dialogs.ConfirmationDialog import com.simplemobiletools.commons.dialogs.RadioGroupDialog import com.simplemobiletools.commons.dialogs.SecurityDialog +import com.simplemobiletools.commons.extensions.beVisibleIf import com.simplemobiletools.commons.extensions.handleHiddenFolderPasswordProtection import com.simplemobiletools.commons.extensions.updateTextColors +import com.simplemobiletools.commons.helpers.PROTECTION_FINGERPRINT import com.simplemobiletools.commons.helpers.SHOW_ALL_TABS import com.simplemobiletools.commons.models.RadioItem import com.simplemobiletools.gallery.R +import com.simplemobiletools.gallery.dialogs.ManageExtendedDetailsDialog import com.simplemobiletools.gallery.extensions.config -import com.simplemobiletools.gallery.helpers.* +import com.simplemobiletools.gallery.helpers.ROTATE_BY_ASPECT_RATIO +import com.simplemobiletools.gallery.helpers.ROTATE_BY_DEVICE_ROTATION +import com.simplemobiletools.gallery.helpers.ROTATE_BY_SYSTEM_SETTING import kotlinx.android.synthetic.main.activity_settings.* class SettingsActivity : SimpleActivity() { @@ -38,10 +44,13 @@ class SettingsActivity : SimpleActivity() { setupDarkBackground() setupScrollHorizontally() setupScreenRotation() + setupHideSystemUI() setupReplaceShare() setupPasswordProtection() - setupShowMedia() - setupHideSystemUI() + setupDeleteEmptyFolders() + setupAllowVideoGestures() + setupShowExtendedDetails() + setupManageExtendedDetails() updateTextColors(settings_holder) } @@ -163,10 +172,32 @@ class SettingsActivity : SimpleActivity() { config.isPasswordProtectionOn = !hasPasswordProtection config.passwordHash = if (hasPasswordProtection) "" else hash config.protectionType = type + + if (config.isPasswordProtectionOn) { + val confirmationTextId = if (config.protectionType == PROTECTION_FINGERPRINT) + R.string.fingerprint_setup_successfully else R.string.protection_setup_successfully + ConfirmationDialog(this, "", confirmationTextId, R.string.ok, 0) { } + } } } } + private fun setupDeleteEmptyFolders() { + settings_delete_empty_folders.isChecked = config.deleteEmptyFolders + settings_delete_empty_folders_holder.setOnClickListener { + settings_delete_empty_folders.toggle() + config.deleteEmptyFolders = settings_delete_empty_folders.isChecked + } + } + + private fun setupAllowVideoGestures() { + settings_allow_video_gestures.isChecked = config.allowVideoGestures + settings_allow_video_gestures_holder.setOnClickListener { + settings_allow_video_gestures.toggle() + config.allowVideoGestures = settings_allow_video_gestures.isChecked + } + } + private fun setupScreenRotation() { settings_screen_rotation.text = getScreenRotationText() settings_screen_rotation_holder.setOnClickListener { @@ -188,24 +219,23 @@ class SettingsActivity : SimpleActivity() { else -> R.string.screen_rotation_aspect_ratio }) - private fun setupShowMedia() { - settings_show_media.text = getShowMediaText() - settings_show_media_holder.setOnClickListener { - val items = arrayListOf( - RadioItem(IMAGES_AND_VIDEOS, res.getString(R.string.images_and_videos)), - RadioItem(IMAGES, res.getString(R.string.images)), - RadioItem(VIDEOS, res.getString(R.string.videos))) - - RadioGroupDialog(this@SettingsActivity, items, config.showMedia) { - config.showMedia = it as Int - settings_show_media.text = getShowMediaText() - } + private fun setupShowExtendedDetails() { + settings_show_extended_details.isChecked = config.showExtendedDetails + settings_show_extended_details_holder.setOnClickListener { + settings_show_extended_details.toggle() + config.showExtendedDetails = settings_show_extended_details.isChecked + settings_manage_extended_details_holder.beVisibleIf(config.showExtendedDetails) } } - private fun getShowMediaText() = getString(when (config.showMedia) { - IMAGES_AND_VIDEOS -> R.string.images_and_videos - IMAGES -> R.string.images - else -> R.string.videos - }) + private fun setupManageExtendedDetails() { + settings_manage_extended_details_holder.beVisibleIf(config.showExtendedDetails) + settings_manage_extended_details_holder.setOnClickListener { + ManageExtendedDetailsDialog(this) { + if (config.extendedDetails == 0) { + settings_show_extended_details_holder.callOnClick() + } + } + } + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/activities/SimpleActivity.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/activities/SimpleActivity.kt index 6cfd6eb38..f30d65e31 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/activities/SimpleActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/activities/SimpleActivity.kt @@ -1,9 +1,12 @@ package com.simplemobiletools.gallery.activities import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.extensions.getFilenameFromPath import com.simplemobiletools.commons.extensions.toast import com.simplemobiletools.gallery.R import com.simplemobiletools.gallery.dialogs.PickDirectoryDialog +import com.simplemobiletools.gallery.extensions.config +import com.simplemobiletools.gallery.models.Directory import java.io.File import java.util.* @@ -19,4 +22,15 @@ open class SimpleActivity : BaseSimpleActivity() { copyMoveFilesTo(files, source.trimEnd('/'), it, isCopyOperation, true, callback) } } + + fun addTempFolderIfNeeded(dirs: ArrayList): ArrayList { + val directories = ArrayList() + val tempFolderPath = config.tempFolderPath + if (tempFolderPath.isNotEmpty()) { + val newFolder = Directory(tempFolderPath, "", tempFolderPath.getFilenameFromPath(), 0, 0, 0, 0L) + directories.add(newFolder) + } + directories.addAll(dirs) + return directories + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/activities/ViewPagerActivity.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/activities/ViewPagerActivity.kt index b88493874..64c315ba4 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/activities/ViewPagerActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/activities/ViewPagerActivity.kt @@ -1,5 +1,7 @@ package com.simplemobiletools.gallery.activities +import android.animation.Animator +import android.animation.ValueAnimator import android.app.Activity import android.content.Intent import android.content.pm.ActivityInfo @@ -20,14 +22,16 @@ import android.provider.MediaStore import android.support.v4.view.ViewPager import android.util.DisplayMetrics import android.view.* -import com.simplemobiletools.commons.dialogs.ConfirmationDialog +import android.view.animation.DecelerateInterpolator import com.simplemobiletools.commons.dialogs.PropertiesDialog import com.simplemobiletools.commons.dialogs.RenameItemDialog import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.helpers.PERMISSION_WRITE_STORAGE import com.simplemobiletools.gallery.R import com.simplemobiletools.gallery.activities.MediaActivity.Companion.mMedia import com.simplemobiletools.gallery.adapters.MyPagerAdapter import com.simplemobiletools.gallery.asynctasks.GetMediaAsynctask +import com.simplemobiletools.gallery.dialogs.DeleteWithRememberDialog import com.simplemobiletools.gallery.dialogs.SaveAsDialog import com.simplemobiletools.gallery.dialogs.SlideshowDialog import com.simplemobiletools.gallery.extensions.* @@ -50,6 +54,7 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View private var mPos = -1 private var mShowAll = false private var mIsSlideshowActive = false + private var mSkipConfirmationDialog = false private var mRotationDegrees = 0f private var mLastHandledOrientation = 0 private var mPrevHashcode = 0 @@ -69,11 +74,17 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View super.onCreate(savedInstanceState) setContentView(R.layout.activity_medium) - if (!hasWriteStoragePermission()) { - finish() - return + handlePermission(PERMISSION_WRITE_STORAGE) { + if (it) { + initViewPager() + } else { + toast(R.string.no_storage_permissions) + finish() + } } + } + private fun initViewPager() { measureScreen() val uri = intent.data if (uri != null) { @@ -88,8 +99,14 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View cursor?.close() } } else { - mPath = intent.getStringExtra(MEDIUM) - mShowAll = config.showAll + try { + mPath = intent.getStringExtra(MEDIUM) + mShowAll = config.showAll + } catch (e: Exception) { + showErrorToast(e) + finish() + return + } } if (mPath.isEmpty()) { @@ -98,10 +115,14 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View return } - if (intent.extras?.containsKey(IS_VIEW_INTENT) == true && File(mPath).isHidden) { - if (!config.isPasswordProtectionOn) { - config.temporarilyShowHidden = true + if (intent.extras?.containsKey(IS_VIEW_INTENT) == true) { + if (isShowHiddenFlagNeeded()) { + if (!config.isPasswordProtectionOn) { + config.temporarilyShowHidden = true + } } + + config.isThirdPartyIntent = true } showSystemUI() @@ -109,17 +130,13 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View mDirectory = File(mPath).parent title = mPath.getFilenameFromPath() - view_pager.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { - override fun onGlobalLayout() { - view_pager.viewTreeObserver.removeOnGlobalLayoutListener(this) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && isDestroyed) - return - + view_pager.onGlobalLayout { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 || !isDestroyed) { if (mMedia.isNotEmpty()) { gotMedia(mMedia) } } - }) + } reloadViewPager() scanPath(mPath) {} @@ -145,28 +162,32 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View if (intent.extras?.containsKey(IS_VIEW_INTENT) == true) { config.temporarilyShowHidden = false } + + if (config.isThirdPartyIntent) { + config.isThirdPartyIntent = false + + if (intent.extras == null || !intent.getBooleanExtra(IS_FROM_GALLERY, false)) { + mMedia.clear() + } + } } private fun setupOrientationEventListener() { mOrientationEventListener = object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_NORMAL) { override fun onOrientationChanged(orientation: Int) { - val currOrient = if (orientation in 45..134) { - ORIENT_LANDSCAPE_RIGHT - } else if (orientation in 225..314) { - ORIENT_LANDSCAPE_LEFT - } else { - ORIENT_PORTRAIT + val currOrient = when (orientation) { + in 45..134 -> ORIENT_LANDSCAPE_RIGHT + in 225..314 -> ORIENT_LANDSCAPE_LEFT + else -> ORIENT_PORTRAIT } if (mLastHandledOrientation != currOrient) { mLastHandledOrientation = currOrient - if (currOrient == ORIENT_LANDSCAPE_LEFT) { - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE - } else if (currOrient == ORIENT_LANDSCAPE_RIGHT) { - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE - } else { - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + requestedOrientation = when (currOrient) { + ORIENT_LANDSCAPE_LEFT -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + ORIENT_LANDSCAPE_RIGHT -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE + else -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } } } @@ -175,8 +196,9 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View override fun onResume() { super.onResume() - if (!hasWriteStoragePermission()) { + if (!hasPermission(PERMISSION_WRITE_STORAGE)) { finish() + return } supportActionBar?.setBackgroundDrawable(resources.getDrawable(R.drawable.actionbar_gradient_background)) @@ -233,7 +255,7 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View R.id.menu_unhide -> toggleFileVisibility(false) R.id.menu_share_1 -> shareMedium(getCurrentMedium()!!) R.id.menu_share_2 -> shareMedium(getCurrentMedium()!!) - R.id.menu_delete -> askConfirmDelete() + R.id.menu_delete -> checkDeleteConfirmation() R.id.menu_rename -> renameFile() R.id.menu_edit -> openFileEditor(getCurrentFile()) R.id.menu_properties -> showProperties() @@ -266,12 +288,8 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View private fun startSlideshow() { if (getMediaForSlideshow()) { - view_pager.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { - override fun onGlobalLayout() { - view_pager.viewTreeObserver.removeOnGlobalLayoutListener(this) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && isDestroyed) - return - + view_pager.onGlobalLayout { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 || !isDestroyed) { hideSystemUI() mSlideshowInterval = config.slideshowInterval mSlideshowMoveBackwards = config.slideshowMoveBackwards @@ -279,7 +297,59 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) scheduleSwipe() } - }) + } + } + } + + private fun animatePagerTransition(forward: Boolean) { + val oldPosition = view_pager.currentItem + val animator = ValueAnimator.ofInt(0, view_pager.width) + animator.addListener(object : Animator.AnimatorListener { + override fun onAnimationRepeat(animation: Animator?) { + } + + override fun onAnimationEnd(animation: Animator?) { + view_pager.endFakeDrag() + + if (view_pager.currentItem == oldPosition) { + slideshowEnded(forward) + } + } + + override fun onAnimationCancel(animation: Animator?) { + view_pager.endFakeDrag() + } + + override fun onAnimationStart(animation: Animator?) { + } + }) + + animator.interpolator = DecelerateInterpolator() + animator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener { + var oldDragPosition = 0 + override fun onAnimationUpdate(animation: ValueAnimator) { + val dragPosition = animation.animatedValue as Int + val dragOffset = dragPosition - oldDragPosition + oldDragPosition = dragPosition + view_pager?.fakeDragBy(dragOffset * (if (forward) 1f else -1f)) + } + }) + + animator.duration = SLIDESHOW_SCROLL_DURATION + view_pager.beginFakeDrag() + animator.start() + } + + private fun slideshowEnded(forward: Boolean) { + if (config.loopSlideshow) { + if (forward) { + view_pager.setCurrentItem(0, false) + } else { + view_pager.setCurrentItem(view_pager.adapter!!.count - 1, false) + } + } else { + stopSlideshow() + toast(R.string.slideshow_ended) } } @@ -308,24 +378,23 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View } private fun swipeToNextMedium() { - val before = view_pager.currentItem - view_pager.currentItem = if (mSlideshowMoveBackwards) --view_pager.currentItem else ++view_pager.currentItem - if (before == view_pager.currentItem) { - stopSlideshow() - toast(R.string.slideshow_ended) - } + animatePagerTransition(!mSlideshowMoveBackwards) } private fun getMediaForSlideshow(): Boolean { mSlideshowMedia = mMedia.toMutableList() if (!config.slideshowIncludePhotos) { - mSlideshowMedia = mSlideshowMedia.filter { !it.isImage() && !it.isGif() } as MutableList + mSlideshowMedia = mSlideshowMedia.filter { !it.isImage() } as MutableList } if (!config.slideshowIncludeVideos) { mSlideshowMedia = mSlideshowMedia.filter { it.isImage() || it.isGif() } as MutableList } + if (!config.slideshowIncludeGIFs) { + mSlideshowMedia = mSlideshowMedia.filter { !it.isGif() } as MutableList + } + if (config.slideshowRandomOrder) { Collections.shuffle(mSlideshowMedia) mPos = 0 @@ -347,6 +416,7 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View private fun copyMoveTo(isCopyOperation: Boolean) { val files = ArrayList(1).apply { add(getCurrentFile()) } tryCopyMoveFilesTo(files, isCopyOperation) { + config.tempFolderPath = "" if (!isCopyOperation) { reloadViewPager() } @@ -368,34 +438,6 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View } private fun rotateImage() { - val currentMedium = getCurrentMedium() ?: return - if (currentMedium.isJpg() && !isPathOnSD(currentMedium.path)) { - rotateByExif() - } else { - rotateByDegrees() - } - } - - private fun rotateByExif() { - val exif = ExifInterface(getCurrentPath()) - val rotation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) - val newRotation = getNewRotation(rotation) - exif.setAttribute(ExifInterface.TAG_ORIENTATION, newRotation) - exif.saveAttributes() - File(getCurrentPath()).setLastModified(System.currentTimeMillis()) - (getCurrentFragment() as? PhotoFragment)?.refreshBitmap() - } - - private fun getNewRotation(rotation: Int): String { - return when (rotation) { - ExifInterface.ORIENTATION_ROTATE_90 -> ExifInterface.ORIENTATION_ROTATE_180 - ExifInterface.ORIENTATION_ROTATE_180 -> ExifInterface.ORIENTATION_ROTATE_270 - ExifInterface.ORIENTATION_ROTATE_270 -> ExifInterface.ORIENTATION_NORMAL - else -> ExifInterface.ORIENTATION_ROTATE_90 - }.toString() - } - - private fun rotateByDegrees() { mRotationDegrees = (mRotationDegrees + 90) % 360 getCurrentFragment()?.let { (it as? PhotoFragment)?.rotateImageViewBy(mRotationDegrees) @@ -409,10 +451,16 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View Thread({ toast(R.string.saving) val selectedFile = File(it) - val tmpFile = File(selectedFile.parent, "tmp_${it.getFilenameFromPath()}") + val tmpFile = File(selectedFile.parent, ".tmp_${it.getFilenameFromPath()}") try { val bitmap = BitmapFactory.decodeFile(currPath) getFileOutputStream(tmpFile) { + if (it == null) { + toast(R.string.unknown_error_occurred) + deleteFile(tmpFile) {} + return@getFileOutputStream + } + saveFile(tmpFile, bitmap, it) if (needsStupidWritePermissions(selectedFile.absolutePath)) { deleteFile(selectedFile) {} @@ -426,7 +474,7 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View toast(R.string.out_of_memory_error) deleteFile(tmpFile) {} } catch (e: Exception) { - toast(R.string.unknown_error_occurred) + showErrorToast(e) deleteFile(tmpFile) {} } }).start() @@ -439,8 +487,30 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View val bmp = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) bmp.compress(file.getCompressionFormat(), 90, out) out.flush() - toast(R.string.file_saved) out.close() + toast(R.string.file_saved) + mRotationDegrees = 0f + invalidateOptionsMenu() + } + + private fun isShowHiddenFlagNeeded(): Boolean { + val file = File(mPath) + if (file.isHidden) + return true + + var parent = file.parentFile ?: return false + while (true) { + if (parent.isHidden || parent.listFiles()?.contains(File(NOMEDIA)) == true) { + return true + } + + if (parent.absolutePath == "/") { + break + } + parent = parent.parentFile ?: return false + } + + return false } private fun getCurrentFragment() = (view_pager.adapter as MyPagerAdapter).getCurrentFragment(view_pager.currentItem) @@ -521,11 +591,24 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View super.onActivityResult(requestCode, resultCode, resultData) } + private fun checkDeleteConfirmation() { + if (mSkipConfirmationDialog) { + deleteConfirmed() + } else { + askConfirmDelete() + } + } + private fun askConfirmDelete() { - ConfirmationDialog(this) { - deleteFileBg(File(getCurrentMedia()[mPos].path)) { - reloadViewPager() - } + DeleteWithRememberDialog(this) { + mSkipConfirmationDialog = it + deleteConfirmed() + } + } + + private fun deleteConfirmed() { + deleteFileBg(File(getCurrentMedia()[mPos].path)) { + reloadViewPager() } } @@ -534,8 +617,9 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View deleteDirectoryIfEmpty() finish() true - } else + } else { false + } } private fun renameFile() { @@ -576,10 +660,10 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View mPrevHashcode = media.hashCode() mMedia = media - if (mPos == -1) { - mPos = getPositionInList(media) + mPos = if (mPos == -1) { + getPositionInList(media) } else { - mPos = Math.min(mPos, mMedia.size - 1) + Math.min(mPos, mMedia.size - 1) } updateActionbarTitle() @@ -590,20 +674,18 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View private fun getPositionInList(items: MutableList): Int { mPos = 0 - var i = 0 - for (medium in items) { + for ((i, medium) in items.withIndex()) { if (medium.path == mPath) { return i } - i++ } return mPos } private fun deleteDirectoryIfEmpty() { val file = File(mDirectory) - if (!file.isDownloadsFolder() && file.isDirectory && file.listFiles()?.isEmpty() == true) { - file.delete() + if (config.deleteEmptyFolders && !file.isDownloadsFolder() && file.isDirectory && file.listFiles()?.isEmpty() == true) { + deleteFile(file, true) {} } scanPath(mDirectory) {} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/adapters/DirectoryAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/adapters/DirectoryAdapter.kt index 3ef268ecf..54565ee11 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/adapters/DirectoryAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/adapters/DirectoryAdapter.kt @@ -1,6 +1,7 @@ package com.simplemobiletools.gallery.adapters import android.graphics.PorterDuff +import android.os.Build import android.support.v7.view.ActionMode import android.support.v7.widget.RecyclerView import android.util.SparseArray @@ -19,9 +20,10 @@ import com.simplemobiletools.gallery.activities.SimpleActivity import com.simplemobiletools.gallery.dialogs.ExcludeFolderDialog import com.simplemobiletools.gallery.dialogs.PickMediumDialog import com.simplemobiletools.gallery.extensions.* +import com.simplemobiletools.gallery.helpers.VIEW_TYPE_LIST import com.simplemobiletools.gallery.models.AlbumCover import com.simplemobiletools.gallery.models.Directory -import kotlinx.android.synthetic.main.directory_item.view.* +import kotlinx.android.synthetic.main.directory_item_list.view.* import java.io.File import java.util.* @@ -30,11 +32,13 @@ class DirectoryAdapter(val activity: SimpleActivity, var dirs: MutableList() val selectedPositions = HashSet() var primaryColor = config.primaryColor + var textColor = config.textColor var pinnedFolders = config.pinnedFolders var scrollVertically = !config.scrollHorizontally @@ -42,8 +46,9 @@ class DirectoryAdapter(val activity: SimpleActivity, var dirs: MutableList = selectedPositions } - val multiSelectorMode = object : ModalMultiSelectorCallback(multiSelector) { + private val multiSelectorMode = object : ModalMultiSelectorCallback(multiSelector) { override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { when (item.itemId) { R.id.cab_properties -> showProperties() @@ -118,11 +123,12 @@ class DirectoryAdapter(val activity: SimpleActivity, var dirs: MutableList 0 @@ -133,11 +139,12 @@ class DirectoryAdapter(val activity: SimpleActivity, var dirs: MutableList 0 @@ -211,13 +218,14 @@ class DirectoryAdapter(val activity: SimpleActivity, var dirs: MutableList() - var curIndex = 0 - for (i in 0..itemViews.size() - 1) { - if (itemViews[i] != null) { - newItems.put(curIndex, itemViews[i]) - curIndex++ - } - } + (0 until itemViews.size()) + .filter { itemViews[it] != null } + .forEachIndexed { curIndex, i -> newItems.put(curIndex, itemViews[i]) } itemViews = newItems + actMode?.finish() } } @@ -301,20 +306,30 @@ class DirectoryAdapter(val activity: SimpleActivity, var dirs: MutableList) { activity.config.albumCovers = Gson().toJson(albumCovers) actMode?.finish() @@ -328,13 +343,14 @@ class DirectoryAdapter(val activity: SimpleActivity, var dirs: MutableList) { dirs = newDirs notifyDataSetChanged() + actMode?.finish() + } + + fun updateTextColor(textColor: Int) { + this.textColor = textColor + notifyDataSetChanged() } fun selectItem(pos: Int) { @@ -367,7 +389,7 @@ class DirectoryAdapter(val activity: SimpleActivity, var dirs: MutableList -1 && min < to) { - (min..to - 1).filter { it != from } + (min until to).filter { it != from } .forEach { toggleItemSelection(false, it) } } if (max > -1) { @@ -384,7 +406,7 @@ class DirectoryAdapter(val activity: SimpleActivity, var dirs: MutableList -1) { - for (i in min..from - 1) + for (i in min until from) toggleItemSelection(false, i) } } @@ -393,44 +415,51 @@ class DirectoryAdapter(val activity: SimpleActivity, var dirs: MutableList (Unit)) : SwappingHolder(view, MultiSelector()) { - fun bindView(directory: Directory, isPinned: Boolean, scrollVertically: Boolean): View { + fun bindView(directory: Directory, isPinned: Boolean, scrollVertically: Boolean, isListView: Boolean, textColor: Int): View { itemView.apply { dir_name.text = directory.name + dir_path?.text = "${directory.path.substringBeforeLast("/")}/" photo_cnt.text = directory.mediaCnt.toString() activity.loadImage(directory.tmb, dir_thumbnail, scrollVertically) dir_pin.beVisibleIf(isPinned) dir_sd_card.beVisibleIf(activity.isPathOnSD(directory.path)) + if (isListView) { + dir_name.setTextColor(textColor) + dir_path.setTextColor(textColor) + photo_cnt.setTextColor(textColor) + dir_pin.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + dir_sd_card.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + } + setOnClickListener { viewClicked(directory) } setOnLongClickListener { if (isPickIntent) viewClicked(directory) else viewLongClicked(); true } - - } return itemView } - fun viewClicked(directory: Directory) { + private fun viewClicked(directory: Directory) { if (multiSelector.isSelectable) { - val isSelected = adapterListener.getSelectedPositions().contains(layoutPosition) - adapterListener.toggleItemSelectionAdapter(!isSelected, layoutPosition) + val isSelected = adapterListener.getSelectedPositions().contains(adapterPosition) + adapterListener.toggleItemSelectionAdapter(!isSelected, adapterPosition) } else { itemClick(directory) } } - fun viewLongClicked() { + private fun viewLongClicked() { if (listener != null) { if (!multiSelector.isSelectable) { activity.startSupportActionMode(multiSelectorCallback) - adapterListener.toggleItemSelectionAdapter(true, layoutPosition) + adapterListener.toggleItemSelectionAdapter(true, adapterPosition) } - listener.itemLongClicked(layoutPosition) + listener.itemLongClicked(adapterPosition) } } fun stopLoad() { - if (!activity.isDestroyed) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 || !activity.isDestroyed) Glide.with(activity).clear(view.dir_thumbnail) } } @@ -447,5 +476,7 @@ class DirectoryAdapter(val activity: SimpleActivity, var dirs: MutableList) fun itemLongClicked(position: Int) + + fun recheckPinnedFolders() } } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/adapters/MediaAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/adapters/MediaAdapter.kt index 9cbe8915e..875609abd 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/adapters/MediaAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/adapters/MediaAdapter.kt @@ -1,6 +1,7 @@ package com.simplemobiletools.gallery.adapters import android.graphics.PorterDuff +import android.os.Build import android.support.v7.view.ActionMode import android.support.v7.widget.RecyclerView import android.util.SparseArray @@ -9,29 +10,33 @@ import com.bignerdranch.android.multiselector.ModalMultiSelectorCallback import com.bignerdranch.android.multiselector.MultiSelector import com.bignerdranch.android.multiselector.SwappingHolder import com.bumptech.glide.Glide -import com.simplemobiletools.commons.dialogs.ConfirmationDialog import com.simplemobiletools.commons.dialogs.PropertiesDialog import com.simplemobiletools.commons.dialogs.RenameItemDialog import com.simplemobiletools.commons.extensions.beGone import com.simplemobiletools.commons.extensions.beVisibleIf import com.simplemobiletools.gallery.R import com.simplemobiletools.gallery.activities.SimpleActivity +import com.simplemobiletools.gallery.dialogs.DeleteWithRememberDialog import com.simplemobiletools.gallery.extensions.* +import com.simplemobiletools.gallery.helpers.VIEW_TYPE_LIST import com.simplemobiletools.gallery.models.Medium -import kotlinx.android.synthetic.main.photo_video_item.view.* +import kotlinx.android.synthetic.main.photo_video_item_grid.view.* import java.io.File import java.util.* class MediaAdapter(val activity: SimpleActivity, var media: MutableList, val listener: MediaOperationsListener?, val isPickIntent: Boolean, - val itemClick: (Medium) -> Unit) : RecyclerView.Adapter() { + val allowMultiplePicks: Boolean, val itemClick: (Medium) -> Unit) : RecyclerView.Adapter() { val multiSelector = MultiSelector() val config = activity.config + val isListViewType = config.viewTypeFiles == VIEW_TYPE_LIST + var skipConfirmationDialog = false var actMode: ActionMode? = null var itemViews = SparseArray() val selectedPositions = HashSet() var primaryColor = config.primaryColor + var textColor = config.textColor var displayFilenames = config.displayFileNames var scrollVertically = !config.scrollHorizontally @@ -39,8 +44,9 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, if (select) { itemViews[pos]?.medium_check?.background?.setColorFilter(primaryColor, PorterDuff.Mode.SRC_IN) selectedPositions.add(pos) - } else + } else { selectedPositions.remove(pos) + } itemViews[pos]?.medium_check?.beVisibleIf(select) @@ -52,12 +58,12 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, updateTitle(selectedPositions.size) } - fun updateTitle(cnt: Int) { + private fun updateTitle(cnt: Int) { actMode?.title = "$cnt / ${media.size}" actMode?.invalidate() } - val adapterListener = object : MyAdapterListener { + private val adapterListener = object : MyAdapterListener { override fun toggleItemSelectionAdapter(select: Boolean, position: Int) { toggleItemSelection(select, position) } @@ -65,9 +71,10 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, override fun getSelectedPositions(): HashSet = selectedPositions } - val multiSelectorMode = object : ModalMultiSelectorCallback(multiSelector) { + private val multiSelectorMode = object : ModalMultiSelectorCallback(multiSelector) { override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { when (item.itemId) { + R.id.cab_confirm_selection -> confirmSelection() R.id.cab_properties -> showProperties() R.id.cab_rename -> renameFile() R.id.cab_edit -> editFile() @@ -77,7 +84,7 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, R.id.cab_copy_to -> copyMoveTo(true) R.id.cab_move_to -> copyMoveTo(false) R.id.cab_select_all -> selectAll() - R.id.cab_delete -> askConfirmDelete() + R.id.cab_delete -> checkDeleteConfirmation() else -> return false } return true @@ -93,6 +100,7 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, override fun onPrepareActionMode(actionMode: ActionMode?, menu: Menu): Boolean { menu.findItem(R.id.cab_rename).isVisible = selectedPositions.size <= 1 menu.findItem(R.id.cab_edit).isVisible = selectedPositions.size == 1 && media.size > selectedPositions.first() && media[selectedPositions.first()].isImage() + menu.findItem(R.id.cab_confirm_selection).isVisible = isPickIntent && allowMultiplePicks && selectedPositions.size > 0 checkHideBtnVisibility(menu) @@ -111,11 +119,12 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, fun checkHideBtnVisibility(menu: Menu) { var hiddenCnt = 0 var unhiddenCnt = 0 - selectedPositions.map { media.getOrNull(it) }.filterNotNull().forEach { - if (it.name.startsWith('.')) + selectedPositions.mapNotNull { media.getOrNull(it) }.forEach { + if (it.name.startsWith('.')) { hiddenCnt++ - else + } else { unhiddenCnt++ + } } menu.findItem(R.id.cab_hide).isVisible = unhiddenCnt > 0 @@ -123,6 +132,11 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, } } + private fun confirmSelection() { + val paths = getSelectedMedia().map { it.path } as ArrayList + listener?.selectedPaths(paths) + } + private fun showProperties() { if (selectedPositions.size <= 1) { PropertiesDialog(activity, media[selectedPositions.first()].path, config.shouldShowHidden) @@ -161,9 +175,9 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, } private fun shareMedia() { - if (selectedPositions.size <= 1) { + if (selectedPositions.size == 1 && selectedPositions.first() != -1) { activity.shareMedium(getSelectedMedia()[0]) - } else { + } else if (selectedPositions.size > 1) { activity.shareMedia(getSelectedMedia()) } } @@ -173,6 +187,7 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, selectedPositions.forEach { files.add(File(media[it].path)) } activity.tryCopyMoveFilesTo(files, isCopyOperation) { + config.tempFolderPath = "" if (!isCopyOperation) { listener?.refreshItems() } @@ -182,7 +197,7 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, fun selectAll() { val cnt = media.size - for (i in 0..cnt - 1) { + for (i in 0 until cnt) { selectedPositions.add(i) multiSelector.setSelected(i, 0, true) notifyItemChanged(i) @@ -190,18 +205,31 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, updateTitle(cnt) } - private fun askConfirmDelete() { - ConfirmationDialog(activity) { - deleteFiles() - actMode?.finish() + private fun checkDeleteConfirmation() { + if (skipConfirmationDialog) { + deleteConfirmed() + } else { + askConfirmDelete() } } + private fun askConfirmDelete() { + DeleteWithRememberDialog(activity) { + skipConfirmationDialog = it + deleteConfirmed() + } + } + + private fun deleteConfirmed() { + deleteFiles() + } + private fun getCurrentFile() = File(media[selectedPositions.first()].path) private fun deleteFiles() { - if (selectedPositions.isEmpty()) + if (selectedPositions.isEmpty()) { return + } val files = ArrayList(selectedPositions.size) val removeMedia = ArrayList(selectedPositions.size) @@ -225,15 +253,12 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, listener?.deleteFiles(files) val newItems = SparseArray() - var curIndex = 0 - for (i in 0..itemViews.size() - 1) { - if (itemViews[i] != null) { - newItems.put(curIndex, itemViews[i]) - curIndex++ - } - } + (0 until itemViews.size()) + .filter { itemViews[it] != null } + .forEachIndexed { curIndex, i -> newItems.put(curIndex, itemViews[i]) } itemViews = newItems + actMode?.finish() } } @@ -244,12 +269,13 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, } override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent?.context).inflate(R.layout.photo_video_item, parent, false) - return ViewHolder(view, adapterListener, activity, multiSelectorMode, multiSelector, listener, isPickIntent, itemClick) + val layoutType = if (isListViewType) R.layout.photo_video_item_list else R.layout.photo_video_item_grid + val view = LayoutInflater.from(parent?.context).inflate(layoutType, parent, false) + return ViewHolder(view, adapterListener, activity, multiSelectorMode, multiSelector, listener, allowMultiplePicks || !isPickIntent, itemClick) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - itemViews.put(position, holder.bindView(media[position], displayFilenames, scrollVertically)) + itemViews.put(position, holder.bindView(media[position], displayFilenames, scrollVertically, isListViewType, textColor)) toggleItemSelection(selectedPositions.contains(position), position) holder.itemView.tag = holder } @@ -264,6 +290,7 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, fun updateMedia(newMedia: ArrayList) { media = newMedia notifyDataSetChanged() + actMode?.finish() } fun updateDisplayFilenames(display: Boolean) { @@ -271,6 +298,11 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, notifyDataSetChanged() } + fun updateTextColor(textColor: Int) { + this.textColor = textColor + notifyDataSetChanged() + } + fun selectItem(pos: Int) { toggleItemSelection(true, pos) } @@ -287,7 +319,7 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, toggleItemSelection(true, i) if (min > -1 && min < to) { - (min..to - 1).filter { it != from } + (min until to).filter { it != from } .forEach { toggleItemSelection(false, it) } } if (max > -1) { @@ -304,50 +336,56 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, } if (min > -1) { - for (i in min..from - 1) + for (i in min until from) toggleItemSelection(false, i) } } } class ViewHolder(val view: View, val adapterListener: MyAdapterListener, val activity: SimpleActivity, val multiSelectorCallback: ModalMultiSelectorCallback, - val multiSelector: MultiSelector, val listener: MediaOperationsListener?, val isPickIntent: Boolean, val itemClick: (Medium) -> (Unit)) : + val multiSelector: MultiSelector, val listener: MediaOperationsListener?, val allowMultiplePicks: Boolean, + val itemClick: (Medium) -> (Unit)) : SwappingHolder(view, MultiSelector()) { - fun bindView(medium: Medium, displayFilenames: Boolean, scrollVertically: Boolean): View { + fun bindView(medium: Medium, displayFilenames: Boolean, scrollVertically: Boolean, isListViewType: Boolean, textColor: Int): View { itemView.apply { play_outline.visibility = if (medium.video) View.VISIBLE else View.GONE - photo_name.beVisibleIf(displayFilenames) + photo_name.beVisibleIf(displayFilenames || isListViewType) photo_name.text = medium.name activity.loadImage(medium.path, medium_thumbnail, scrollVertically) + if (isListViewType) { + photo_name.setTextColor(textColor) + play_outline.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + } + setOnClickListener { viewClicked(medium) } - setOnLongClickListener { if (isPickIntent) viewClicked(medium) else viewLongClicked(); true } + setOnLongClickListener { if (allowMultiplePicks) viewLongClicked() else viewClicked(medium); true } } return itemView } - fun viewClicked(medium: Medium) { + private fun viewClicked(medium: Medium) { if (multiSelector.isSelectable) { - val isSelected = adapterListener.getSelectedPositions().contains(layoutPosition) - adapterListener.toggleItemSelectionAdapter(!isSelected, layoutPosition) + val isSelected = adapterListener.getSelectedPositions().contains(adapterPosition) + adapterListener.toggleItemSelectionAdapter(!isSelected, adapterPosition) } else { itemClick(medium) } } - fun viewLongClicked() { + private fun viewLongClicked() { if (listener != null) { if (!multiSelector.isSelectable) { activity.startSupportActionMode(multiSelectorCallback) - adapterListener.toggleItemSelectionAdapter(true, layoutPosition) + adapterListener.toggleItemSelectionAdapter(true, adapterPosition) } - listener.itemLongClicked(layoutPosition) + listener.itemLongClicked(adapterPosition) } } fun stopLoad() { - if (!activity.isDestroyed) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 || !activity.isDestroyed) Glide.with(activity).clear(view.medium_thumbnail) } } @@ -364,5 +402,7 @@ class MediaAdapter(val activity: SimpleActivity, var media: MutableList, fun deleteFiles(files: ArrayList) fun itemLongClicked(position: Int) + + fun selectedPaths(paths: ArrayList) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/asynctasks/GetDirectoriesAsynctask.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/asynctasks/GetDirectoriesAsynctask.kt index ebc8182f0..d1767ce4a 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/asynctasks/GetDirectoriesAsynctask.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/asynctasks/GetDirectoriesAsynctask.kt @@ -3,126 +3,74 @@ package com.simplemobiletools.gallery.asynctasks import android.content.Context import android.os.AsyncTask import com.simplemobiletools.commons.extensions.getFilenameFromPath -import com.simplemobiletools.commons.extensions.hasWriteStoragePermission +import com.simplemobiletools.commons.extensions.hasPermission import com.simplemobiletools.commons.extensions.internalStoragePath import com.simplemobiletools.commons.extensions.sdCardPath +import com.simplemobiletools.commons.helpers.PERMISSION_WRITE_STORAGE +import com.simplemobiletools.commons.helpers.SORT_DESCENDING import com.simplemobiletools.gallery.R import com.simplemobiletools.gallery.extensions.config import com.simplemobiletools.gallery.extensions.containsNoMedia -import com.simplemobiletools.gallery.extensions.getFilesFrom +import com.simplemobiletools.gallery.extensions.sumByLong +import com.simplemobiletools.gallery.helpers.MediaFetcher import com.simplemobiletools.gallery.models.Directory import com.simplemobiletools.gallery.models.Medium import java.io.File -import java.util.* class GetDirectoriesAsynctask(val context: Context, val isPickVideo: Boolean, val isPickImage: Boolean, val callback: (dirs: ArrayList) -> Unit) : AsyncTask>() { - var config = context.config - var shouldStop = false - val showHidden = config.shouldShowHidden + val mediaFetcher = MediaFetcher(context) override fun doInBackground(vararg params: Void): ArrayList { - if (!context.hasWriteStoragePermission()) + if (!context.hasPermission(PERMISSION_WRITE_STORAGE)) return ArrayList() - val media = context.getFilesFrom("", isPickImage, isPickVideo) - val excludedPaths = config.excludedFolders - val includedPaths = config.includedFolders - val directories = groupDirectories(media) - val dirs = ArrayList(directories.values - .filter { File(it.path).exists() }) - .filter { shouldFolderBeVisible(it.path, excludedPaths, includedPaths) } as ArrayList - Directory.sorting = config.directorySorting - dirs.sort() - return movePinnedToFront(dirs) - } - - private fun groupDirectories(media: ArrayList): Map { - val albumCovers = config.parseAlbumCovers() + val config = context.config + val groupedMedia = mediaFetcher.getMediaByDirectories(isPickVideo, isPickImage) + val directories = ArrayList() val hidden = context.resources.getString(R.string.hidden) - val directories = LinkedHashMap() - for ((name, path, isVideo, dateModified, dateTaken, size) in media) { - if (shouldStop) - cancel(true) + val albumCovers = config.parseAlbumCovers() + for ((path, curMedia) in groupedMedia) { + Medium.sorting = config.getFileSorting(path) + curMedia.sort() - val parentDir = File(path).parent ?: continue - if (directories.containsKey(parentDir.toLowerCase())) { - val directory = directories[parentDir.toLowerCase()]!! - val newImageCnt = directory.mediaCnt + 1 - directory.mediaCnt = newImageCnt - directory.addSize(size) - } else { - var dirName = parentDir.getFilenameFromPath() - if (parentDir == context.internalStoragePath) { - dirName = context.getString(R.string.internal) - } else if (parentDir == context.sdCardPath) { - dirName = context.getString(R.string.sd_card) + val firstItem = curMedia.first() + val lastItem = curMedia.last() + val parentDir = File(firstItem.path).parent + var thumbnail = firstItem.path + albumCovers.forEach { + if (it.path == parentDir && File(it.tmb).exists()) { + thumbnail = it.tmb } - - if (File(parentDir).containsNoMedia()) { - dirName += " $hidden" - - if (!showHidden) - continue - } - - var thumbnail = path - albumCovers.forEach { - if (it.path == parentDir && File(it.tmb).exists()) { - thumbnail = it.tmb - } - } - - val directory = Directory(parentDir, thumbnail, dirName, 1, dateModified, dateTaken, size) - directories.put(parentDir.toLowerCase(), directory) } + + var dirName = when (parentDir) { + context.internalStoragePath -> context.getString(R.string.internal) + context.sdCardPath -> context.getString(R.string.sd_card) + else -> parentDir.getFilenameFromPath() + } + + if (File(parentDir).containsNoMedia()) { + dirName += " $hidden" + } + + val lastModified = if (config.directorySorting and SORT_DESCENDING > 0) Math.max(firstItem.modified, lastItem.modified) else Math.min(firstItem.modified, lastItem.modified) + val dateTaken = if (config.directorySorting and SORT_DESCENDING > 0) Math.max(firstItem.taken, lastItem.taken) else Math.min(firstItem.taken, lastItem.taken) + val size = curMedia.sumByLong { it.size } + val directory = Directory(parentDir, thumbnail, dirName, curMedia.size, lastModified, dateTaken, size) + directories.add(directory) } + return directories } - private fun shouldFolderBeVisible(path: String, excludedPaths: MutableSet, includedPaths: MutableSet): Boolean { - val file = File(path) - return if (isThisOrParentExcluded(path, excludedPaths, includedPaths)) { - false - } else if (!config.shouldShowHidden && file.isDirectory && file.canonicalFile == file.absoluteFile) { - var containsNoMediaOrDot = file.containsNoMedia() || path.contains("/.") - if (!containsNoMediaOrDot) { - containsNoMediaOrDot = checkParentHasNoMedia(file.parentFile) - } - !containsNoMediaOrDot - } else { - true - } - } - - private fun checkParentHasNoMedia(file: File): Boolean { - var curFile = file - while (true) { - if (curFile.containsNoMedia()) { - return true - } - curFile = curFile.parentFile - if (curFile.absolutePath == "/") - break - } - return false - } - - private fun isThisOrParentExcluded(path: String, excludedPaths: MutableSet, includedPaths: MutableSet) = - includedPaths.none { path.startsWith(it) } && excludedPaths.any { path.startsWith(it) } - - private fun movePinnedToFront(dirs: ArrayList): ArrayList { - val foundFolders = ArrayList() - val pinnedFolders = config.pinnedFolders - - dirs.forEach { if (pinnedFolders.contains(it.path)) foundFolders.add(it) } - dirs.removeAll(foundFolders) - dirs.addAll(0, foundFolders) - return dirs - } - override fun onPostExecute(dirs: ArrayList) { super.onPostExecute(dirs) - callback.invoke(dirs) + callback(dirs) + } + + fun stopFetching() { + mediaFetcher.shouldStop = true + cancel(true) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/asynctasks/GetMediaAsynctask.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/asynctasks/GetMediaAsynctask.kt index 89342dc00..a3b8a2b5d 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/asynctasks/GetMediaAsynctask.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/asynctasks/GetMediaAsynctask.kt @@ -2,21 +2,40 @@ package com.simplemobiletools.gallery.asynctasks import android.content.Context import android.os.AsyncTask -import com.simplemobiletools.gallery.extensions.getFilesFrom +import com.simplemobiletools.gallery.extensions.config +import com.simplemobiletools.gallery.helpers.MediaFetcher import com.simplemobiletools.gallery.models.Medium import java.util.* class GetMediaAsynctask(val context: Context, val mPath: String, val isPickVideo: Boolean = false, val isPickImage: Boolean = false, val showAll: Boolean, val callback: (media: ArrayList) -> Unit) : AsyncTask>() { + private val mediaFetcher = MediaFetcher(context) override fun doInBackground(vararg params: Void): ArrayList { - val path = if (showAll) "" else mPath - return context.getFilesFrom(path, isPickImage, isPickVideo) + return if (showAll) { + val mediaMap = mediaFetcher.getMediaByDirectories(isPickVideo, isPickImage) + val media = ArrayList() + + mediaMap.values.forEach { + media.addAll(it) + } + + Medium.sorting = context.config.getFileSorting("") + media.sort() + media + } else { + mediaFetcher.getFilesFrom(mPath, isPickImage, isPickVideo) + } } override fun onPostExecute(media: ArrayList) { super.onPostExecute(media) - callback.invoke(media) + callback(media) + } + + fun stopFetching() { + mediaFetcher.shouldStop = true + cancel(true) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/ChangeSortingDialog.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/ChangeSortingDialog.kt index f7020498d..95513a98e 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/ChangeSortingDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/ChangeSortingDialog.kt @@ -42,12 +42,12 @@ class ChangeSortingDialog(val activity: SimpleActivity, val isDirectorySorting: val sortingRadio = view.sorting_dialog_radio_sorting var sortBtn = sortingRadio.sorting_dialog_radio_name - if (currSorting and SORT_BY_SIZE != 0) { - sortBtn = sortingRadio.sorting_dialog_radio_size - } else if (currSorting and SORT_BY_DATE_MODIFIED != 0) { - sortBtn = sortingRadio.sorting_dialog_radio_last_modified - } else if (currSorting and SORT_BY_DATE_TAKEN != 0) - sortBtn = sortingRadio.sorting_dialog_radio_date_taken + when { + currSorting and SORT_BY_PATH != 0 -> sortBtn = sortingRadio.sorting_dialog_radio_path + currSorting and SORT_BY_SIZE != 0 -> sortBtn = sortingRadio.sorting_dialog_radio_size + currSorting and SORT_BY_DATE_MODIFIED != 0 -> sortBtn = sortingRadio.sorting_dialog_radio_last_modified + currSorting and SORT_BY_DATE_TAKEN != 0 -> sortBtn = sortingRadio.sorting_dialog_radio_date_taken + } sortBtn.isChecked = true } @@ -65,6 +65,7 @@ class ChangeSortingDialog(val activity: SimpleActivity, val isDirectorySorting: val sortingRadio = view.sorting_dialog_radio_sorting var sorting = when (sortingRadio.checkedRadioButtonId) { R.id.sorting_dialog_radio_name -> SORT_BY_NAME + R.id.sorting_dialog_radio_path -> SORT_BY_PATH R.id.sorting_dialog_radio_size -> SORT_BY_SIZE R.id.sorting_dialog_radio_last_modified -> SORT_BY_DATE_MODIFIED else -> SORT_BY_DATE_TAKEN diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/DeleteWithRememberDialog.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/DeleteWithRememberDialog.kt new file mode 100644 index 000000000..05a240577 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/DeleteWithRememberDialog.kt @@ -0,0 +1,28 @@ +package com.simplemobiletools.gallery.dialogs + +import android.content.Context +import android.support.v7.app.AlertDialog +import android.view.LayoutInflater +import com.simplemobiletools.commons.extensions.setupDialogStuff +import com.simplemobiletools.gallery.R +import kotlinx.android.synthetic.main.dialog_delete_with_remember.view.* + +class DeleteWithRememberDialog(val context: Context, val callback: (remember: Boolean) -> Unit) { + var dialog: AlertDialog + val view = LayoutInflater.from(context).inflate(R.layout.dialog_delete_with_remember, null) + + init { + val builder = AlertDialog.Builder(context) + .setPositiveButton(R.string.yes, { dialog, which -> dialogConfirmed() }) + .setNegativeButton(R.string.no, null) + + dialog = builder.create().apply { + context.setupDialogStuff(view, this) + } + } + + private fun dialogConfirmed() { + dialog.dismiss() + callback(view.delete_remember_checkbox.isChecked) + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/FilterMediaDialog.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/FilterMediaDialog.kt new file mode 100644 index 000000000..f99fc98c3 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/FilterMediaDialog.kt @@ -0,0 +1,46 @@ +package com.simplemobiletools.gallery.dialogs + +import android.support.v7.app.AlertDialog +import android.view.LayoutInflater +import android.view.View +import com.simplemobiletools.commons.extensions.setupDialogStuff +import com.simplemobiletools.gallery.R +import com.simplemobiletools.gallery.activities.SimpleActivity +import com.simplemobiletools.gallery.extensions.config +import com.simplemobiletools.gallery.helpers.GIFS +import com.simplemobiletools.gallery.helpers.IMAGES +import com.simplemobiletools.gallery.helpers.VIDEOS +import kotlinx.android.synthetic.main.dialog_filter_media.view.* + +class FilterMediaDialog(val activity: SimpleActivity, val callback: (result: Int) -> Unit) { + private var view: View = LayoutInflater.from(activity).inflate(R.layout.dialog_filter_media, null) + + init { + val filterMedia = activity.config.filterMedia + view.apply { + filter_media_images.isChecked = filterMedia and IMAGES != 0 + filter_media_videos.isChecked = filterMedia and VIDEOS != 0 + filter_media_gifs.isChecked = filterMedia and GIFS != 0 + } + + AlertDialog.Builder(activity) + .setPositiveButton(R.string.ok, { dialog, which -> dialogConfirmed() }) + .setNegativeButton(R.string.cancel, null) + .create().apply { + activity.setupDialogStuff(view, this, R.string.filter_media) + } + } + + private fun dialogConfirmed() { + var result = 0 + if (view.filter_media_images.isChecked) + result += IMAGES + if (view.filter_media_videos.isChecked) + result += VIDEOS + if (view.filter_media_gifs.isChecked) + result += GIFS + + activity.config.filterMedia = result + callback(result) + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/ManageExtendedDetailsDialog.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/ManageExtendedDetailsDialog.kt new file mode 100644 index 000000000..3d8c5e3a7 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/ManageExtendedDetailsDialog.kt @@ -0,0 +1,70 @@ +package com.simplemobiletools.gallery.dialogs + +import android.support.v7.app.AlertDialog +import android.view.LayoutInflater +import android.view.View +import com.simplemobiletools.commons.extensions.setupDialogStuff +import com.simplemobiletools.gallery.R +import com.simplemobiletools.gallery.activities.SimpleActivity +import com.simplemobiletools.gallery.extensions.config +import com.simplemobiletools.gallery.helpers.* +import kotlinx.android.synthetic.main.dialog_manage_extended_details.view.* + +class ManageExtendedDetailsDialog(val activity: SimpleActivity, val callback: (result: Int) -> Unit) { + private var view: View = LayoutInflater.from(activity).inflate(R.layout.dialog_manage_extended_details, null) + + init { + val details = activity.config.extendedDetails + view.apply { + manage_extended_details_name.isChecked = details and EXT_NAME != 0 + manage_extended_details_path.isChecked = details and EXT_PATH != 0 + manage_extended_details_size.isChecked = details and EXT_SIZE != 0 + manage_extended_details_resolution.isChecked = details and EXT_RESOLUTION != 0 + manage_extended_details_last_modified.isChecked = details and EXT_LAST_MODIFIED != 0 + manage_extended_details_date_taken.isChecked = details and EXT_DATE_TAKEN != 0 + manage_extended_details_camera.isChecked = details and EXT_CAMERA_MODEL != 0 + manage_extended_details_exif.isChecked = details and EXT_EXIF_PROPERTIES != 0 + manage_extended_details_duration.isChecked = details and EXT_DURATION != 0 + manage_extended_details_artist.isChecked = details and EXT_ARTIST != 0 + manage_extended_details_album.isChecked = details and EXT_ALBUM != 0 + } + + AlertDialog.Builder(activity) + .setPositiveButton(R.string.ok, { dialog, which -> dialogConfirmed() }) + .setNegativeButton(R.string.cancel, null) + .create().apply { + activity.setupDialogStuff(view, this) + } + } + + private fun dialogConfirmed() { + var result = 0 + view.apply { + if (manage_extended_details_name.isChecked) + result += EXT_NAME + if (manage_extended_details_path.isChecked) + result += EXT_PATH + if (manage_extended_details_size.isChecked) + result += EXT_SIZE + if (manage_extended_details_resolution.isChecked) + result += EXT_RESOLUTION + if (manage_extended_details_last_modified.isChecked) + result += EXT_LAST_MODIFIED + if (manage_extended_details_date_taken.isChecked) + result += EXT_DATE_TAKEN + if (manage_extended_details_camera.isChecked) + result += EXT_CAMERA_MODEL + if (manage_extended_details_exif.isChecked) + result += EXT_EXIF_PROPERTIES + if (manage_extended_details_duration.isChecked) + result += EXT_DURATION + if (manage_extended_details_artist.isChecked) + result += EXT_ARTIST + if (manage_extended_details_album.isChecked) + result += EXT_ALBUM + } + + activity.config.extendedDetails = result + callback(result) + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/PickDirectoryDialog.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/PickDirectoryDialog.kt index 39d0ca7f2..e72a3395c 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/PickDirectoryDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/PickDirectoryDialog.kt @@ -3,7 +3,6 @@ package com.simplemobiletools.gallery.dialogs import android.support.v7.app.AlertDialog import android.support.v7.widget.GridLayoutManager import android.view.LayoutInflater -import android.view.View import com.simplemobiletools.commons.dialogs.FilePickerDialog import com.simplemobiletools.commons.extensions.beGoneIf import com.simplemobiletools.commons.extensions.beVisibleIf @@ -15,18 +14,21 @@ import com.simplemobiletools.gallery.adapters.DirectoryAdapter import com.simplemobiletools.gallery.asynctasks.GetDirectoriesAsynctask import com.simplemobiletools.gallery.extensions.config import com.simplemobiletools.gallery.extensions.getCachedDirectories +import com.simplemobiletools.gallery.extensions.getSortedDirectories +import com.simplemobiletools.gallery.helpers.VIEW_TYPE_GRID import com.simplemobiletools.gallery.models.Directory import kotlinx.android.synthetic.main.dialog_directory_picker.view.* class PickDirectoryDialog(val activity: SimpleActivity, val sourcePath: String, val callback: (path: String) -> Unit) { var dialog: AlertDialog - var shownDirectories: ArrayList = ArrayList() - var view: View = LayoutInflater.from(activity).inflate(R.layout.dialog_directory_picker, null) + var shownDirectories = ArrayList() + var view = LayoutInflater.from(activity).inflate(R.layout.dialog_directory_picker, null) + var isGridViewType = activity.config.viewTypeFolders == VIEW_TYPE_GRID init { (view.directories_grid.layoutManager as GridLayoutManager).apply { - orientation = if (activity.config.scrollHorizontally) GridLayoutManager.HORIZONTAL else GridLayoutManager.VERTICAL - spanCount = activity.config.dirColumnCnt + orientation = if (activity.config.scrollHorizontally && isGridViewType) GridLayoutManager.HORIZONTAL else GridLayoutManager.VERTICAL + spanCount = if (isGridViewType) activity.config.dirColumnCnt else 1 } dialog = AlertDialog.Builder(activity) @@ -35,31 +37,32 @@ class PickDirectoryDialog(val activity: SimpleActivity, val sourcePath: String, .setNeutralButton(R.string.other_folder, { dialogInterface, i -> showOtherFolder() }) .create().apply { activity.setupDialogStuff(view, this, R.string.select_destination) - - val dirs = activity.getCachedDirectories() - if (dirs.isNotEmpty()) { - gotDirectories(dirs) - } - - GetDirectoriesAsynctask(activity, false, false) { - gotDirectories(it) - }.execute() } + + val dirs = activity.getCachedDirectories() + if (dirs.isNotEmpty()) { + gotDirectories(activity.addTempFolderIfNeeded(dirs)) + } + + GetDirectoriesAsynctask(activity, false, false) { + gotDirectories(activity.addTempFolderIfNeeded(it)) + }.execute() } - fun showOtherFolder() { + private fun showOtherFolder() { val showHidden = activity.config.shouldShowHidden FilePickerDialog(activity, sourcePath, false, showHidden, true) { callback(it) } } - private fun gotDirectories(directories: ArrayList) { - if (directories.hashCode() == shownDirectories.hashCode()) + private fun gotDirectories(newDirs: ArrayList) { + val dirs = activity.getSortedDirectories(newDirs) + if (dirs.hashCode() == shownDirectories.hashCode()) return - shownDirectories = directories - val adapter = DirectoryAdapter(activity, directories, null, true) { + shownDirectories = dirs + val adapter = DirectoryAdapter(activity, dirs, null, true) { if (it.path.trimEnd('/') == sourcePath) { activity.toast(R.string.source_and_destination_same) return@DirectoryAdapter @@ -69,7 +72,7 @@ class PickDirectoryDialog(val activity: SimpleActivity, val sourcePath: String, } } - val scrollHorizontally = activity.config.scrollHorizontally + val scrollHorizontally = activity.config.scrollHorizontally && isGridViewType view.apply { directories_grid.adapter = adapter diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/PickMediumDialog.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/PickMediumDialog.kt index 4c57c68f1..3596a6907 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/PickMediumDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/PickMediumDialog.kt @@ -1,44 +1,55 @@ package com.simplemobiletools.gallery.dialogs import android.support.v7.app.AlertDialog -import android.support.v7.widget.RecyclerView +import android.support.v7.widget.GridLayoutManager import android.view.LayoutInflater -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken +import com.simplemobiletools.commons.extensions.beGoneIf +import com.simplemobiletools.commons.extensions.beVisibleIf import com.simplemobiletools.commons.extensions.setupDialogStuff import com.simplemobiletools.gallery.R import com.simplemobiletools.gallery.activities.SimpleActivity import com.simplemobiletools.gallery.adapters.MediaAdapter import com.simplemobiletools.gallery.asynctasks.GetMediaAsynctask import com.simplemobiletools.gallery.extensions.config +import com.simplemobiletools.gallery.extensions.getCachedMedia +import com.simplemobiletools.gallery.helpers.VIEW_TYPE_GRID import com.simplemobiletools.gallery.models.Medium import kotlinx.android.synthetic.main.dialog_medium_picker.view.* class PickMediumDialog(val activity: SimpleActivity, val path: String, val callback: (path: String) -> Unit) { var dialog: AlertDialog - var mediaGrid: RecyclerView - var shownMedia: ArrayList = ArrayList() + var shownMedia = ArrayList() + val view = LayoutInflater.from(activity).inflate(R.layout.dialog_medium_picker, null) + var isGridViewType = activity.config.viewTypeFiles == VIEW_TYPE_GRID init { - val view = LayoutInflater.from(activity).inflate(R.layout.dialog_medium_picker, null) - mediaGrid = view.media_grid + (view.media_grid.layoutManager as GridLayoutManager).apply { + orientation = if (activity.config.scrollHorizontally && isGridViewType) GridLayoutManager.HORIZONTAL else GridLayoutManager.VERTICAL + spanCount = if (isGridViewType) activity.config.mediaColumnCnt else 1 + } dialog = AlertDialog.Builder(activity) .setPositiveButton(R.string.ok, null) .setNegativeButton(R.string.cancel, null) + .setNeutralButton(R.string.other_folder, { dialogInterface, i -> showOtherFolder() }) .create().apply { activity.setupDialogStuff(view, this, R.string.select_photo) + } - val token = object : TypeToken>() {}.type - val media = Gson().fromJson>(activity.config.loadFolderMedia(path), token) ?: ArrayList(1) + val media = activity.getCachedMedia(path).filter { !it.video } as ArrayList + if (media.isNotEmpty()) { + gotMedia(media) + } - if (media.isNotEmpty()) { - gotMedia(media) - } + GetMediaAsynctask(activity, path, false, true, false) { + gotMedia(it) + }.execute() + } - GetMediaAsynctask(activity, path, false, true, false) { - gotMedia(it) - }.execute() + private fun showOtherFolder() { + PickDirectoryDialog(activity, path) { + callback(it) + dialog.dismiss() } } @@ -47,10 +58,26 @@ class PickMediumDialog(val activity: SimpleActivity, val path: String, val callb return shownMedia = media - val adapter = MediaAdapter(activity, media, null, true) { + val adapter = MediaAdapter(activity, media, null, true, false) { callback(it.path) dialog.dismiss() } - mediaGrid.adapter = adapter + + val scrollHorizontally = activity.config.scrollHorizontally && isGridViewType + view.apply { + media_grid.adapter = adapter + + media_vertical_fastscroller.isHorizontal = false + media_vertical_fastscroller.beGoneIf(scrollHorizontally) + + media_horizontal_fastscroller.isHorizontal = true + media_horizontal_fastscroller.beVisibleIf(scrollHorizontally) + + if (scrollHorizontally) { + media_horizontal_fastscroller.setViews(media_grid) + } else { + media_vertical_fastscroller.setViews(media_grid) + } + } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/SlideshowDialog.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/SlideshowDialog.kt index f28d8ef62..493e8a980 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/SlideshowDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/dialogs/SlideshowDialog.kt @@ -6,15 +6,14 @@ import android.view.View import android.view.WindowManager import com.simplemobiletools.commons.extensions.hideKeyboard import com.simplemobiletools.commons.extensions.setupDialogStuff +import com.simplemobiletools.commons.extensions.toast import com.simplemobiletools.gallery.R import com.simplemobiletools.gallery.activities.SimpleActivity import com.simplemobiletools.gallery.extensions.config import com.simplemobiletools.gallery.helpers.SLIDESHOW_DEFAULT_INTERVAL import kotlinx.android.synthetic.main.dialog_slideshow.view.* - class SlideshowDialog(val activity: SimpleActivity, val callback: () -> Unit) { - val dialog: AlertDialog val view: View init { @@ -42,6 +41,11 @@ class SlideshowDialog(val activity: SimpleActivity, val callback: () -> Unit) { include_videos.toggle() } + include_gifs_holder.setOnClickListener { + interval_value.clearFocus() + include_gifs.toggle() + } + random_order_holder.setOnClickListener { interval_value.clearFocus() random_order.toggle() @@ -56,15 +60,30 @@ class SlideshowDialog(val activity: SimpleActivity, val callback: () -> Unit) { interval_value.clearFocus() move_backwards.toggle() } + + loop_slideshow_holder.setOnClickListener { + interval_value.clearFocus() + loop_slideshow.toggle() + } } setupValues() - dialog = AlertDialog.Builder(activity) - .setPositiveButton(R.string.ok, { dialog, which -> dialogConfirmed() }) + AlertDialog.Builder(activity) + .setPositiveButton(R.string.ok, null) .setNegativeButton(R.string.cancel, null) .create().apply { window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN) activity.setupDialogStuff(view, this) + getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener({ + if (!view.include_photos.isChecked && !view.include_videos.isChecked && !view.include_gifs.isChecked) { + activity.toast(R.string.no_media_for_slideshow) + return@setOnClickListener + } + + storeValues() + callback() + dismiss() + }) } } @@ -74,13 +93,15 @@ class SlideshowDialog(val activity: SimpleActivity, val callback: () -> Unit) { interval_value.setText(config.slideshowInterval.toString()) include_photos.isChecked = config.slideshowIncludePhotos include_videos.isChecked = config.slideshowIncludeVideos + include_gifs.isChecked = config.slideshowIncludeGIFs random_order.isChecked = config.slideshowRandomOrder use_fade.isChecked = config.slideshowUseFade move_backwards.isChecked = config.slideshowMoveBackwards + loop_slideshow.isChecked = config.loopSlideshow } } - private fun dialogConfirmed() { + private fun storeValues() { var interval = view.interval_value.text.toString() if (interval.trim('0').isEmpty()) interval = SLIDESHOW_DEFAULT_INTERVAL.toString() @@ -89,11 +110,11 @@ class SlideshowDialog(val activity: SimpleActivity, val callback: () -> Unit) { slideshowInterval = interval.toInt() slideshowIncludePhotos = view.include_photos.isChecked slideshowIncludeVideos = view.include_videos.isChecked + slideshowIncludeGIFs = view.include_gifs.isChecked slideshowRandomOrder = view.random_order.isChecked slideshowUseFade = view.use_fade.isChecked slideshowMoveBackwards = view.move_backwards.isChecked + loopSlideshow = view.loop_slideshow.isChecked } - dialog.dismiss() - callback() } } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/extensions/activity.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/extensions/activity.kt index ddb92198c..0a2140a6d 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/extensions/activity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/extensions/activity.kt @@ -24,6 +24,7 @@ import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.gallery.BuildConfig import com.simplemobiletools.gallery.R import com.simplemobiletools.gallery.activities.SimpleActivity +import com.simplemobiletools.gallery.helpers.IS_FROM_GALLERY import com.simplemobiletools.gallery.helpers.NOMEDIA import com.simplemobiletools.gallery.helpers.REQUEST_EDIT_IMAGE import com.simplemobiletools.gallery.helpers.REQUEST_SET_AS @@ -48,6 +49,7 @@ fun Activity.shareMedium(medium: Medium) { val shareTitle = resources.getString(R.string.share_via) val file = File(medium.path) val uri = Uri.fromFile(file) + Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_STREAM, uri) @@ -59,14 +61,12 @@ fun Activity.shareMedium(medium: Medium) { fun Activity.shareMedia(media: List) { val shareTitle = resources.getString(R.string.share_via) - val uris = ArrayList(media.size) + val uris = media.map { Uri.fromFile(File(it.path)) } as ArrayList + Intent().apply { action = Intent.ACTION_SEND_MULTIPLE type = "image/* video/*" addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - media.map { File(it.path) } - .mapTo(uris) { Uri.fromFile(it) } - putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris) startActivity(Intent.createChooser(this, shareTitle)) } @@ -92,14 +92,14 @@ fun Activity.setAs(uri: Uri, file: File, showToast: Boolean = true): Boolean { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) val chooser = Intent.createChooser(this, getString(R.string.set_as)) - if (resolveActivity(packageManager) != null) { + success = if (resolveActivity(packageManager) != null) { startActivityForResult(chooser, REQUEST_SET_AS) - success = true + true } else { if (showToast) { toast(R.string.no_capable_app_found) } - success = false + false } } @@ -131,6 +131,7 @@ fun Activity.openWith(file: File, forceChooser: Boolean = true) { action = Intent.ACTION_VIEW setDataAndType(uri, file.getMimeType()) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + putExtra(IS_FROM_GALLERY, true) if (resolveActivity(packageManager) != null) { val chooser = Intent.createChooser(this, getString(R.string.open_with)) @@ -223,9 +224,10 @@ fun SimpleActivity.addNoMedia(path: String, callback: () -> Unit) { if (needsStupidWritePermissions(path)) { handleSAFDialog(file) { - try { - getFileDocument(path)?.createFile("", NOMEDIA) - } catch (e: Exception) { + val fileDocument = getFileDocument(path) + if (fileDocument?.exists() == true && fileDocument.isDirectory) { + fileDocument.createFile("", NOMEDIA) + } else { toast(R.string.unknown_error_occurred) } } @@ -233,9 +235,10 @@ fun SimpleActivity.addNoMedia(path: String, callback: () -> Unit) { try { file.createNewFile() } catch (e: Exception) { - toast(R.string.unknown_error_occurred) + showErrorToast(e) } } + scanFile(file) { callback() } @@ -251,10 +254,10 @@ fun SimpleActivity.removeNoMedia(path: String, callback: () -> Unit) { fun SimpleActivity.toggleFileVisibility(oldFile: File, hide: Boolean, callback: (newFile: File) -> Unit) { val path = oldFile.parent var filename = oldFile.name - if (hide) { - filename = ".${filename.trimStart('.')}" + filename = if (hide) { + ".${filename.trimStart('.')}" } else { - filename = filename.substring(1, filename.length) + filename.substring(1, filename.length) } val newFile = File(path, filename) renameFile(oldFile, newFile) { @@ -336,3 +339,8 @@ fun Activity.getCachedDirectories(): ArrayList { val token = object : TypeToken>() {}.type return Gson().fromJson>(config.directories, token) ?: ArrayList(1) } + +fun Activity.getCachedMedia(path: String): ArrayList { + val token = object : TypeToken>() {}.type + return Gson().fromJson>(config.loadFolderMedia(path), token) ?: ArrayList(1) +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/extensions/arrayList.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/extensions/arrayList.kt new file mode 100644 index 000000000..8d5d3ad27 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/extensions/arrayList.kt @@ -0,0 +1,5 @@ +package com.simplemobiletools.gallery.extensions + +import java.util.* + +fun ArrayList.sumByLong(selector: (E) -> Long) = map { selector(it) }.sum() diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/extensions/context.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/extensions/context.kt index 2fd607c32..f7cd61529 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/extensions/context.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/extensions/context.kt @@ -4,25 +4,45 @@ import android.content.Context import android.content.Intent import android.content.res.Configuration import android.database.Cursor +import android.graphics.Point import android.media.AudioManager import android.net.Uri +import android.os.Build import android.provider.MediaStore -import com.simplemobiletools.commons.extensions.* -import com.simplemobiletools.commons.helpers.SORT_BY_DATE_MODIFIED -import com.simplemobiletools.commons.helpers.SORT_BY_NAME -import com.simplemobiletools.commons.helpers.SORT_BY_SIZE -import com.simplemobiletools.commons.helpers.SORT_DESCENDING +import android.view.WindowManager +import com.simplemobiletools.commons.extensions.humanizePath import com.simplemobiletools.gallery.activities.SettingsActivity import com.simplemobiletools.gallery.helpers.Config -import com.simplemobiletools.gallery.helpers.IMAGES -import com.simplemobiletools.gallery.helpers.NOMEDIA -import com.simplemobiletools.gallery.helpers.VIDEOS -import com.simplemobiletools.gallery.models.Medium -import java.io.File -import java.util.* +import com.simplemobiletools.gallery.models.Directory val Context.portrait get() = resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT val Context.audioManager get() = getSystemService(Context.AUDIO_SERVICE) as AudioManager +val Context.windowManager: WindowManager get() = getSystemService(Context.WINDOW_SERVICE) as WindowManager +val Context.navigationBarRight: Boolean get() = usableScreenSize.x < realScreenSize.x +val Context.navigationBarBottom: Boolean get() = usableScreenSize.y < realScreenSize.y +val Context.navigationBarHeight: Int get() = if (navigationBarBottom) navigationBarSize.y else 0 + +internal val Context.navigationBarSize: Point + get() = when { + navigationBarRight -> Point(realScreenSize.x - usableScreenSize.x, usableScreenSize.y) + navigationBarBottom -> Point(usableScreenSize.x, realScreenSize.y - usableScreenSize.y) + else -> Point() + } + +val Context.usableScreenSize: Point + get() { + val size = Point() + windowManager.defaultDisplay.getSize(size) + return size + } + +val Context.realScreenSize: Point + get() { + val size = Point() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + windowManager.defaultDisplay.getRealSize(size) + return size + } fun Context.getRealPathFromURI(uri: Uri): String? { var cursor: Cursor? = null @@ -51,197 +71,24 @@ fun Context.launchSettings() { val Context.config: Config get() = Config.newInstance(this) -fun Context.getFilesFrom(curPath: String, isPickImage: Boolean, isPickVideo: Boolean): ArrayList { - val projection = arrayOf(MediaStore.Images.Media._ID, - MediaStore.Images.Media.DISPLAY_NAME, - MediaStore.Images.Media.DATE_TAKEN, - MediaStore.Images.Media.DATE_MODIFIED, - MediaStore.Images.Media.DATA, - MediaStore.Images.Media.SIZE) - val uri = MediaStore.Files.getContentUri("external") - val selection = if (curPath.isEmpty()) null else "(${MediaStore.Images.Media.DATA} LIKE ? AND ${MediaStore.Images.Media.DATA} NOT LIKE ?)" - val selectionArgs = if (curPath.isEmpty()) null else arrayOf("$curPath/%", "$curPath/%/%") +fun Context.movePinnedDirectoriesToFront(dirs: ArrayList): ArrayList { + val foundFolders = ArrayList() + val pinnedFolders = config.pinnedFolders - try { - val cur = contentResolver.query(uri, projection, selection, selectionArgs, getSortingForFolder(curPath)) - return parseCursor(this, cur, isPickImage, isPickVideo, curPath) - } catch (e: Exception) { - return ArrayList() - } -} - -private fun parseCursor(context: Context, cur: Cursor, isPickImage: Boolean, isPickVideo: Boolean, curPath: String): ArrayList { - val curMedia = ArrayList() - val config = context.config - val showMedia = config.showMedia - val showHidden = config.shouldShowHidden - val includedFolders = config.includedFolders - val excludedFolders = config.excludedFolders - val noMediaFolders = context.getNoMediaFolders() - - cur.use { cur -> - if (cur.moveToFirst()) { - do { - try { - val path = cur.getStringValue(MediaStore.Images.Media.DATA) - var size = cur.getLongValue(MediaStore.Images.Media.SIZE) - if (size == 0L) { - size = File(path).length() - } - - if (size <= 0L) { - continue - } - - var filename = cur.getStringValue(MediaStore.Images.Media.DISPLAY_NAME) ?: "" - if (filename.isEmpty()) - filename = path.getFilenameFromPath() - - val isImage = filename.isImageFast() || filename.isGif() - val isVideo = if (isImage) false else filename.isVideoFast() - - if (!isImage && !isVideo) - continue - - if (isVideo && (isPickImage || showMedia == IMAGES)) - continue - - if (isImage && (isPickVideo || showMedia == VIDEOS)) - continue - - if (!showHidden && filename.startsWith('.')) - continue - - var isExcluded = false - excludedFolders.forEach { - if (path.startsWith("$it/")) { - isExcluded = true - includedFolders.forEach { - if (path.startsWith("$it/")) { - isExcluded = false - } - } - } - } - - if (!isExcluded && !showHidden) { - noMediaFolders.forEach { - if (path.startsWith("$it/")) { - isExcluded = true - } - } - } - - if (!isExcluded && !showHidden && path.contains("/.")) { - isExcluded = true - } - - if (!isExcluded) { - val dateTaken = cur.getLongValue(MediaStore.Images.Media.DATE_TAKEN) - val dateModified = cur.getIntValue(MediaStore.Images.Media.DATE_MODIFIED) * 1000L - - val medium = Medium(filename, path, isVideo, dateModified, dateTaken, size) - curMedia.add(medium) - } - } catch (e: Exception) { - continue - } - } while (cur.moveToNext()) - } + dirs.forEach { + if (pinnedFolders.contains(it.path)) + foundFolders.add(it) } - config.includedFolders.filter { it.isNotEmpty() && (curPath.isEmpty() || it == curPath) }.mapNotNull { File(it).listFiles() }.forEach { - for (file in it) { - val size = file.length() - if (size <= 0L) { - continue - } - - val filename = file.name - val isImage = filename.isImageFast() || filename.isGif() - val isVideo = if (isImage) false else filename.isVideoFast() - - if (!isImage && !isVideo) - continue - - if (isVideo && (isPickImage || showMedia == IMAGES)) - continue - - if (isImage && (isPickVideo || showMedia == VIDEOS)) - continue - - val dateTaken = file.lastModified() - val dateModified = file.lastModified() - - val medium = Medium(filename, file.absolutePath, isVideo, dateModified, dateTaken, size) - val isAlreadyAdded = curMedia.any { it.path == file.absolutePath } - if (!isAlreadyAdded) - curMedia.add(medium) - } - } - - Medium.sorting = config.getFileSorting(curPath) - curMedia.sort() - - return curMedia + dirs.removeAll(foundFolders) + dirs.addAll(0, foundFolders) + return dirs } -fun Context.getSortingForFolder(path: String): String { - val sorting = config.getFileSorting(path) - val sortValue = if (sorting and SORT_BY_NAME > 0) - MediaStore.Images.Media.DISPLAY_NAME - else if (sorting and SORT_BY_SIZE > 0) - MediaStore.Images.Media.SIZE - else if (sorting and SORT_BY_DATE_MODIFIED > 0) - MediaStore.Images.Media.DATE_MODIFIED - else - MediaStore.Images.Media.DATE_TAKEN - - return if (sorting and SORT_DESCENDING > 0) - "$sortValue DESC" - else - "$sortValue ASC" -} - -fun Context.getNoMediaFolders(): ArrayList { - val folders = ArrayList() - val noMediaCondition = "${MediaStore.Files.FileColumns.MEDIA_TYPE} = ${MediaStore.Files.FileColumns.MEDIA_TYPE_NONE}" - - val uri = MediaStore.Files.getContentUri("external") - val columns = arrayOf(MediaStore.Files.FileColumns.DATA) - val where = "$noMediaCondition AND ${MediaStore.Files.FileColumns.TITLE} LIKE ?" - val args = arrayOf("%$NOMEDIA%") - var cursor: Cursor? = null - - try { - cursor = contentResolver.query(uri, columns, where, args, null) - if (cursor?.moveToFirst() == true) { - do { - val path = cursor.getString(cursor.getColumnIndex(MediaStore.Files.FileColumns.DATA)) ?: continue - val noMediaFile = File(path) - if (noMediaFile.exists()) - folders.add(noMediaFile.parent) - } while (cursor.moveToNext()) - } - } finally { - cursor?.close() - } - - return folders -} - -fun Context.getLastMediaModified(): Int { - val max = "max" - val uri = MediaStore.Files.getContentUri("external") - val projection = arrayOf(MediaStore.Images.Media._ID, "MAX(${MediaStore.Images.Media.DATE_MODIFIED}) AS $max") - var cursor: Cursor? = null - try { - cursor = contentResolver.query(uri, projection, null, null, null) - if (cursor?.moveToFirst() == true) { - return cursor.getIntValue(max) - } - } finally { - cursor?.close() - } - return 0 +@Suppress("UNCHECKED_CAST") +fun Context.getSortedDirectories(source: ArrayList): ArrayList { + Directory.sorting = config.directorySorting + val dirs = source.clone() as ArrayList + dirs.sort() + return movePinnedDirectoriesToFront(dirs) } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/fragments/PhotoFragment.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/fragments/PhotoFragment.kt index 3d634a516..a96db9a4a 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/fragments/PhotoFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/fragments/PhotoFragment.kt @@ -24,15 +24,10 @@ import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.Target import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView -import com.simplemobiletools.commons.extensions.beGone -import com.simplemobiletools.commons.extensions.beVisible -import com.simplemobiletools.commons.extensions.toast +import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.gallery.R import com.simplemobiletools.gallery.activities.ViewPagerActivity -import com.simplemobiletools.gallery.extensions.config -import com.simplemobiletools.gallery.extensions.getFileSignature -import com.simplemobiletools.gallery.extensions.getRealPathFromURI -import com.simplemobiletools.gallery.extensions.portrait +import com.simplemobiletools.gallery.extensions.* import com.simplemobiletools.gallery.helpers.GlideRotateTransformation import com.simplemobiletools.gallery.helpers.MEDIUM import com.simplemobiletools.gallery.models.Medium @@ -43,10 +38,13 @@ import java.io.FileOutputStream import java.io.IOException class PhotoFragment : ViewPagerFragment() { - lateinit var medium: Medium - lateinit var view: ViewGroup private var isFragmentVisible = false private var wasInit = false + private var storedShowExtendedDetails = false + private var storedExtendedDetails = 0 + + lateinit var view: ViewGroup + lateinit var medium: Medium override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { view = inflater.inflate(R.layout.pager_photo_item, container, false) as ViewGroup @@ -101,12 +99,26 @@ class PhotoFragment : ViewPagerFragment() { } } loadImage() + checkExtendedDetails() wasInit = true return view } + override fun onPause() { + super.onPause() + storedShowExtendedDetails = context.config.showExtendedDetails + storedExtendedDetails = context.config.extendedDetails + } + + override fun onResume() { + super.onResume() + if (wasInit && (context.config.showExtendedDetails != storedShowExtendedDetails || context.config.extendedDetails != storedExtendedDetails)) { + checkExtendedDetails() + } + } + override fun setMenuVisibility(menuVisible: Boolean) { super.setMenuVisibility(menuVisible) isFragmentVisible = menuVisible @@ -123,13 +135,11 @@ class PhotoFragment : ViewPagerFragment() { } } - private fun degreesForRotation(orientation: Int): Int { - return when (orientation) { - 8 -> 270 - 3 -> 180 - 6 -> 90 - else -> 0 - } + private fun degreesForRotation(orientation: Int) = when (orientation) { + 8 -> 270 + 3 -> 180 + 6 -> 90 + else -> 0 } private fun rotateViaMatrix(original: Bitmap, orientation: Int): Bitmap { @@ -168,7 +178,7 @@ class PhotoFragment : ViewPagerFragment() { val options = RequestOptions() .signature(medium.path.getFileSignature()) .format(DecodeFormat.PREFER_ARGB_8888) - .diskCacheStrategy(DiskCacheStrategy.NONE) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .override(targetWidth, targetHeight) Glide.with(this) @@ -176,9 +186,7 @@ class PhotoFragment : ViewPagerFragment() { .load(medium.path) .apply(options) .listener(object : RequestListener { - override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean { - return false - } + override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean) = false override fun onResourceReady(resource: Bitmap?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { if (isFragmentVisible) @@ -203,6 +211,7 @@ class PhotoFragment : ViewPagerFragment() { private fun addZoomableView() { if ((medium.isImage()) && isFragmentVisible && view.subsampling_view.visibility == View.GONE) { view.subsampling_view.apply { + //setBitmapDecoderClass(GlideDecoder::class.java) // causing random crashes on Android 7+ maxScale = 10f beVisible() setImage(ImageSource.uri(medium.path)) @@ -222,7 +231,7 @@ class PhotoFragment : ViewPagerFragment() { override fun onPreviewReleased() { } - override fun onImageLoadError(e: Exception?) { + override fun onImageLoadError(e: Exception) { background = ColorDrawable(Color.TRANSPARENT) beGone() } @@ -256,18 +265,32 @@ class PhotoFragment : ViewPagerFragment() { } } - fun refreshBitmap() { - view.subsampling_view.beGone() - loadBitmap() - } - fun rotateImageViewBy(degrees: Float) { view.subsampling_view.beGone() loadBitmap(degrees) } + private fun checkExtendedDetails() { + if (context.config.showExtendedDetails) { + view.photo_details.apply { + text = getMediumExtendedDetails(medium) + setTextColor(context.config.textColor) + beVisible() + onGlobalLayout { + if (height != 0) { + val smallMargin = resources.getDimension(R.dimen.small_margin) + y = context.usableScreenSize.y - height - if (context.navigationBarHeight == 0) smallMargin else 0f + } + } + } + } else { + view.photo_details.beGone() + } + } + override fun onDestroyView() { super.onDestroyView() + context.isKitkatPlus() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && !activity.isDestroyed) { Glide.with(context).clear(view.photo_view) } @@ -276,6 +299,7 @@ class PhotoFragment : ViewPagerFragment() { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) loadImage() + checkExtendedDetails() } private fun photoClicked() { @@ -283,6 +307,13 @@ class PhotoFragment : ViewPagerFragment() { } override fun fullscreenToggled(isFullscreen: Boolean) { - + view.photo_details.apply { + if (visibility == View.VISIBLE) { + val smallMargin = resources.getDimension(R.dimen.small_margin) + val fullscreenOffset = context.navigationBarHeight.toFloat() - smallMargin + val newY = context.usableScreenSize.y - height + if (isFullscreen) fullscreenOffset else -(if (context.navigationBarHeight == 0) smallMargin else 0f) + animate().y(newY) + } + } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/fragments/VideoFragment.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/fragments/VideoFragment.kt index f93bc617e..9d2e395f1 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/fragments/VideoFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/fragments/VideoFragment.kt @@ -14,15 +14,10 @@ import android.view.* import android.view.animation.AnimationUtils import android.widget.SeekBar import android.widget.TextView -import com.simplemobiletools.commons.extensions.getFormattedDuration -import com.simplemobiletools.commons.extensions.toast -import com.simplemobiletools.commons.extensions.updateTextColors +import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.gallery.R import com.simplemobiletools.gallery.activities.ViewPagerActivity -import com.simplemobiletools.gallery.extensions.audioManager -import com.simplemobiletools.gallery.extensions.config -import com.simplemobiletools.gallery.extensions.getNavBarHeight -import com.simplemobiletools.gallery.extensions.hasNavBar +import com.simplemobiletools.gallery.extensions.* import com.simplemobiletools.gallery.helpers.MEDIUM import com.simplemobiletools.gallery.models.Medium import kotlinx.android.synthetic.main.pager_video_item.view.* @@ -38,13 +33,14 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee private var mCurrTimeView: TextView? = null private var mTimerHandler: Handler? = null private var mSeekBar: SeekBar? = null - private var mTimeHolder: View? = null private var mIsPlaying = false private var mIsDragged = false private var mIsFullscreen = false private var mIsFragmentVisible = false private var mPlayOnPrepare = false + private var mStoredShowExtendedDetails = false + private var mStoredExtendedDetails = 0 private var mCurrTime = 0 private var mDuration = 0 @@ -61,6 +57,7 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee lateinit var mView: View lateinit var medium: Medium + lateinit var mTimeHolder: View companion object { private val TAG = VideoFragment::class.java.simpleName @@ -69,9 +66,10 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { mView = inflater.inflate(R.layout.pager_video_item, container, false) - setupPlayer() - + mTimeHolder = mView.video_time_holder medium = arguments.getSerializable(MEDIUM) as Medium + + setupPlayer() if (savedInstanceState != null) { mCurrTime = savedInstanceState.getInt(PROGRESS) } @@ -85,6 +83,27 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee override fun onResume() { super.onResume() activity.updateTextColors(mView.video_holder) + mView.video_volume_controller.beVisibleIf(context.config.allowVideoGestures) + mView.video_brightness_controller.beVisibleIf(context.config.allowVideoGestures) + + if (context.config.showExtendedDetails != mStoredShowExtendedDetails || context.config.extendedDetails != mStoredExtendedDetails) { + checkExtendedDetails() + } + } + + override fun onPause() { + super.onPause() + pauseVideo() + mIsFragmentVisible = false + mStoredShowExtendedDetails = context.config.showExtendedDetails + mStoredExtendedDetails = context.config.extendedDetails + } + + override fun onDestroy() { + super.onDestroy() + if (activity?.isChangingConfigurations == false) { + cleanup() + } } private fun setupPlayer() { @@ -109,6 +128,7 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee } initTimeHolder() + checkExtendedDetails() } override fun setMenuVisibility(menuVisible: Boolean) { @@ -131,6 +151,7 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee super.onConfigurationChanged(newConfig) setVideoSize() initTimeHolder() + checkExtendedDetails() } private fun toggleFullscreen() { @@ -151,7 +172,7 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee val diffX = mTouchDownX - event.x val diffY = mTouchDownY - event.y - if (Math.abs(diffY) > Math.abs(diffX)) { + if (Math.abs(diffY) > 20 && Math.abs(diffY) > Math.abs(diffX)) { var percent = ((diffY / ViewPagerActivity.screenHeight) * 100).toInt() * 3 percent = Math.min(100, Math.max(-100, percent)) @@ -165,7 +186,9 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee mLastTouchY = event.y } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { - if (System.currentTimeMillis() - mTouchDownTime < CLICK_MAX_DURATION) { + val diffX = Math.abs(event.x - mTouchDownX) + val diffY = Math.abs(event.y - mTouchDownY) + if (System.currentTimeMillis() - mTouchDownTime < CLICK_MAX_DURATION && diffX < 20 && diffY < 20) { mView.video_holder.performClick() } } @@ -187,7 +210,7 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee val diffX = mTouchDownX - event.x val diffY = mTouchDownY - event.y - if (Math.abs(diffY) > Math.abs(diffX)) { + if (Math.abs(diffY) > 20 && Math.abs(diffY) > Math.abs(diffX)) { var percent = ((diffY / ViewPagerActivity.screenHeight) * 100).toInt() * 3 percent = Math.min(100, Math.max(-100, percent)) @@ -201,7 +224,9 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee mLastTouchY = event.y } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { - if (System.currentTimeMillis() - mTouchDownTime < CLICK_MAX_DURATION) { + val diffX = Math.abs(event.x - mTouchDownX) + val diffY = Math.abs(event.y - mTouchDownY) + if (System.currentTimeMillis() - mTouchDownTime < CLICK_MAX_DURATION && diffX < 20 && diffY < 20) { mView.video_holder.performClick() } mTouchDownBrightness = mTempBrightness @@ -257,11 +282,10 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee } private fun initTimeHolder() { - mTimeHolder = mView.video_time_holder val res = resources - val height = res.getNavBarHeight() - val left = mTimeHolder!!.paddingLeft - val top = mTimeHolder!!.paddingTop + val height = context.navigationBarHeight + val left = mTimeHolder.paddingLeft + val top = mTimeHolder.paddingTop var right = res.getDimension(R.dimen.timer_padding).toInt() var bottom = 0 @@ -270,8 +294,9 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee bottom += height } else { right += height + bottom += context.navigationBarHeight } - mTimeHolder!!.setPadding(left, top, right, bottom) + mTimeHolder.setPadding(left, top, right, bottom) } mCurrTimeView = mView.video_curr_time @@ -279,7 +304,7 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee mSeekBar!!.setOnSeekBarChangeListener(this) if (mIsFullscreen) - mTimeHolder!!.visibility = View.INVISIBLE + mTimeHolder.beInvisible() } private fun setupTimeHolder() { @@ -323,7 +348,7 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee AnimationUtils.loadAnimation(activity, anim).apply { duration = 150 fillAfter = true - mTimeHolder!!.startAnimation(this) + mTimeHolder.startAnimation(this) } } @@ -388,19 +413,6 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee mMediaPlayer!!.pause() } - override fun onPause() { - super.onPause() - pauseVideo() - mIsFragmentVisible = false - } - - override fun onDestroy() { - super.onDestroy() - if (activity?.isChangingConfigurations == false) { - cleanup() - } - } - private fun cleanup() { pauseVideo() mCurrTimeView?.text = 0.getFormattedDuration() @@ -489,6 +501,25 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee } } + private fun checkExtendedDetails() { + if (context.config.showExtendedDetails) { + mView.video_details.apply { + text = getMediumExtendedDetails(medium) + setTextColor(context.config.textColor) + beVisible() + onGlobalLayout { + if (height != 0) { + val smallMargin = resources.getDimension(R.dimen.small_margin) + val timeHolderHeight = mTimeHolder.height - context.navigationBarHeight + y = context.usableScreenSize.y - height - timeHolderHeight - if (context.navigationBarHeight == 0) smallMargin else 0f + } + } + } + } else { + mView.video_details.beGone() + } + } + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { if (mMediaPlayer != null && fromUser) { setProgress(progress) @@ -517,5 +548,14 @@ class VideoFragment : ViewPagerFragment(), SurfaceHolder.Callback, SeekBar.OnSee override fun fullscreenToggled(isFullscreen: Boolean) { mIsFullscreen = isFullscreen checkFullscreen() + mView.video_details.apply { + if (visibility == View.VISIBLE) { + val smallMargin = resources.getDimension(R.dimen.small_margin) + val timeHolderHeight = mTimeHolder.height - context.navigationBarHeight.toFloat() + val fullscreenOffset = context.navigationBarHeight.toFloat() - smallMargin + val newY = context.usableScreenSize.y - height + if (mIsFullscreen) fullscreenOffset else -(timeHolderHeight + if (context.navigationBarHeight == 0) smallMargin else 0f) + animate().y(newY) + } + } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/fragments/ViewPagerFragment.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/fragments/ViewPagerFragment.kt index 6121151ba..31a2888c4 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/fragments/ViewPagerFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/fragments/ViewPagerFragment.kt @@ -1,6 +1,11 @@ package com.simplemobiletools.gallery.fragments import android.support.v4.app.Fragment +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.gallery.extensions.config +import com.simplemobiletools.gallery.helpers.* +import com.simplemobiletools.gallery.models.Medium +import java.io.File abstract class ViewPagerFragment : Fragment() { var listener: FragmentListener? = null @@ -12,4 +17,44 @@ abstract class ViewPagerFragment : Fragment() { fun videoEnded(): Boolean } + + fun getMediumExtendedDetails(medium: Medium): String { + val file = File(medium.path) + val path = "${file.parent.trimEnd('/')}/" + val exif = android.media.ExifInterface(medium.path) + val details = StringBuilder() + val detailsFlag = context.config.extendedDetails + if (detailsFlag and EXT_NAME != 0) { + medium.name.let { if (it.isNotEmpty()) details.appendln(it) } + } + + if (detailsFlag and EXT_PATH != 0) { + path.let { if (it.isNotEmpty()) details.appendln(it) } + } + + if (detailsFlag and EXT_SIZE != 0) { + file.length().formatSize().let { if (it.isNotEmpty()) details.appendln(it) } + } + + if (detailsFlag and EXT_RESOLUTION != 0) { + file.getResolution().formatAsResolution().let { if (it.isNotEmpty()) details.appendln(it) } + } + + if (detailsFlag and EXT_LAST_MODIFIED != 0) { + file.lastModified().formatLastModified().let { if (it.isNotEmpty()) details.appendln(it) } + } + + if (detailsFlag and EXT_DATE_TAKEN != 0) { + path.getExifDateTaken(exif).let { if (it.isNotEmpty()) details.appendln(it) } + } + + if (detailsFlag and EXT_CAMERA_MODEL != 0) { + path.getExifCameraModel(exif).let { if (it.isNotEmpty()) details.appendln(it) } + } + + if (detailsFlag and EXT_EXIF_PROPERTIES != 0) { + path.getExifProperties(exif).let { if (it.isNotEmpty()) details.appendln(it) } + } + return details.toString().trim() + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/helpers/Config.kt index 167988674..7302b3491 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/helpers/Config.kt @@ -28,17 +28,17 @@ class Config(context: Context) : BaseConfig(context) { if (path.isEmpty()) { fileSorting = value } else { - prefs.edit().putInt(SORT_FOLDER_PREFIX + path, value).apply() + prefs.edit().putInt(SORT_FOLDER_PREFIX + path.toLowerCase(), value).apply() } } - fun getFileSorting(path: String) = prefs.getInt(SORT_FOLDER_PREFIX + path, fileSorting) + fun getFileSorting(path: String) = prefs.getInt(SORT_FOLDER_PREFIX + path.toLowerCase(), fileSorting) fun removeFileSorting(path: String) { - prefs.edit().remove(SORT_FOLDER_PREFIX + path).apply() + prefs.edit().remove(SORT_FOLDER_PREFIX + path.toLowerCase()).apply() } - fun hasCustomSorting(path: String) = prefs.contains(SORT_FOLDER_PREFIX + path) + fun hasCustomSorting(path: String) = prefs.contains(SORT_FOLDER_PREFIX + path.toLowerCase()) var wasHideFolderTooltipShown: Boolean get() = prefs.getBoolean(HIDE_FOLDER_TOOLTIP_SHOWN, false) @@ -54,6 +54,10 @@ class Config(context: Context) : BaseConfig(context) { get() = prefs.getBoolean(TEMPORARILY_SHOW_HIDDEN, false) set(temporarilyShowHidden) = prefs.edit().putBoolean(TEMPORARILY_SHOW_HIDDEN, temporarilyShowHidden).apply() + var isThirdPartyIntent: Boolean + get() = prefs.getBoolean(IS_THIRD_PARTY_INTENT, false) + set(isThirdPartyIntent) = prefs.edit().putBoolean(IS_THIRD_PARTY_INTENT, isThirdPartyIntent).apply() + var pinnedFolders: Set get() = prefs.getStringSet(PINNED_FOLDERS, HashSet()) set(pinnedFolders) = prefs.edit().putStringSet(PINNED_FOLDERS, pinnedFolders).apply() @@ -156,9 +160,9 @@ class Config(context: Context) : BaseConfig(context) { get() = prefs.getBoolean(DARK_BACKGROUND, false) set(darkBackground) = prefs.edit().putBoolean(DARK_BACKGROUND, darkBackground).apply() - var showMedia: Int - get() = prefs.getInt(SHOW_MEDIA, IMAGES_AND_VIDEOS) - set(showMedia) = prefs.edit().putInt(SHOW_MEDIA, showMedia).apply() + var filterMedia: Int + get() = prefs.getInt(FILTER_MEDIA, IMAGES or VIDEOS or GIFS) + set(filterMedia) = prefs.edit().putInt(FILTER_MEDIA, filterMedia).apply() var dirColumnCnt: Int get() = prefs.getInt(getDirectoryColumnsField(), getDefaultDirectoryColumnCount()) @@ -233,6 +237,14 @@ class Config(context: Context) : BaseConfig(context) { get() = prefs.getBoolean(REPLACE_SHARE_WITH_ROTATE, false) set(replaceShare) = prefs.edit().putBoolean(REPLACE_SHARE_WITH_ROTATE, replaceShare).apply() + var deleteEmptyFolders: Boolean + get() = prefs.getBoolean(DELETE_EMPTY_FOLDERS, true) + set(deleteEmptyFolders) = prefs.edit().putBoolean(DELETE_EMPTY_FOLDERS, deleteEmptyFolders).apply() + + var allowVideoGestures: Boolean + get() = prefs.getBoolean(ALLOW_VIDEO_GESTURES, true) + set(allowVideoGestures) = prefs.edit().putBoolean(ALLOW_VIDEO_GESTURES, allowVideoGestures).apply() + var slideshowInterval: Int get() = prefs.getInt(SLIDESHOW_INTERVAL, SLIDESHOW_DEFAULT_INTERVAL) set(slideshowInterval) = prefs.edit().putInt(SLIDESHOW_INTERVAL, slideshowInterval).apply() @@ -245,6 +257,10 @@ class Config(context: Context) : BaseConfig(context) { get() = prefs.getBoolean(SLIDESHOW_INCLUDE_VIDEOS, false) set(slideshowIncludeVideos) = prefs.edit().putBoolean(SLIDESHOW_INCLUDE_VIDEOS, slideshowIncludeVideos).apply() + var slideshowIncludeGIFs: Boolean + get() = prefs.getBoolean(SLIDESHOW_INCLUDE_GIFS, false) + set(slideshowIncludeGIFs) = prefs.edit().putBoolean(SLIDESHOW_INCLUDE_GIFS, slideshowIncludeGIFs).apply() + var slideshowRandomOrder: Boolean get() = prefs.getBoolean(SLIDESHOW_RANDOM_ORDER, false) set(slideshowRandomOrder) = prefs.edit().putBoolean(SLIDESHOW_RANDOM_ORDER, slideshowRandomOrder).apply() @@ -256,4 +272,28 @@ class Config(context: Context) : BaseConfig(context) { var slideshowMoveBackwards: Boolean get() = prefs.getBoolean(SLIDESHOW_MOVE_BACKWARDS, false) set(slideshowMoveBackwards) = prefs.edit().putBoolean(SLIDESHOW_MOVE_BACKWARDS, slideshowMoveBackwards).apply() + + var loopSlideshow: Boolean + get() = prefs.getBoolean(SLIDESHOW_LOOP, false) + set(loopSlideshow) = prefs.edit().putBoolean(SLIDESHOW_LOOP, loopSlideshow).apply() + + var tempFolderPath: String + get() = prefs.getString(TEMP_FOLDER_PATH, "") + set(tempFolderPath) = prefs.edit().putString(TEMP_FOLDER_PATH, tempFolderPath).apply() + + var viewTypeFolders: Int + get() = prefs.getInt(VIEW_TYPE_FOLDERS, VIEW_TYPE_GRID) + set(viewTypeFolders) = prefs.edit().putInt(VIEW_TYPE_FOLDERS, viewTypeFolders).apply() + + var viewTypeFiles: Int + get() = prefs.getInt(VIEW_TYPE_FILES, VIEW_TYPE_GRID) + set(viewTypeFiles) = prefs.edit().putInt(VIEW_TYPE_FILES, viewTypeFiles).apply() + + var showExtendedDetails: Boolean + get() = prefs.getBoolean(SHOW_EXTENDED_DETAILS, false) + set(showExtendedDetails) = prefs.edit().putBoolean(SHOW_EXTENDED_DETAILS, showExtendedDetails).apply() + + var extendedDetails: Int + get() = prefs.getInt(EXTENDED_DETAILS, EXT_RESOLUTION or EXT_LAST_MODIFIED or EXT_EXIF_PROPERTIES) + set(extendedDetails) = prefs.edit().putInt(EXTENDED_DETAILS, extendedDetails).apply() } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/helpers/Constants.kt index 3efd48714..0ccfacf64 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/helpers/Constants.kt @@ -6,6 +6,7 @@ val DIRECTORY_SORT_ORDER = "directory_sort_order" val SORT_FOLDER_PREFIX = "sort_folder_" val SHOW_HIDDEN_MEDIA = "show_hidden_media" val TEMPORARILY_SHOW_HIDDEN = "temporarily_show_hidden" +val IS_THIRD_PARTY_INTENT = "is_third_party_intent" val AUTOPLAY_VIDEOS = "autoplay_videos" val LOOP_VIDEOS = "loop_videos" val ANIMATE_GIFS = "animate_gifs" @@ -15,6 +16,7 @@ val SCREEN_ROTATION = "screen_rotation" val DISPLAY_FILE_NAMES = "display_file_names" val DARK_BACKGROUND = "dark_background" val PINNED_FOLDERS = "pinned_folders" +val FILTER_MEDIA = "filter_media" val DIR_COLUMN_CNT = "dir_column_cnt" val DIR_LANDSCAPE_COLUMN_CNT = "dir_landscape_column_cnt" val DIR_HORIZONTAL_COLUMN_CNT = "dir_horizontal_column_cnt" @@ -24,7 +26,6 @@ val MEDIA_LANDSCAPE_COLUMN_CNT = "media_landscape_column_cnt" val MEDIA_HORIZONTAL_COLUMN_CNT = "media_horizontal_column_cnt" val MEDIA_LANDSCAPE_HORIZONTAL_COLUMN_CNT = "media_landscape_horizontal_column_cnt" val SHOW_ALL = "show_all" // display images and videos from all folders together -val SHOW_MEDIA = "show_media" val SAVE_FOLDER_PREFIX = "folder2_" val HIDE_FOLDER_TOOLTIP_SHOWN = "hide_folder_tooltip_shown" val EXCLUDED_FOLDERS = "excluded_folders" @@ -33,15 +34,25 @@ val ALBUM_COVERS = "album_covers" val SCROLL_HORIZONTALLY = "scroll_horizontally" val HIDE_SYSTEM_UI = "hide_system_ui" val REPLACE_SHARE_WITH_ROTATE = "replace_share_with_rotate" +val DELETE_EMPTY_FOLDERS = "delete_empty_folders" +val ALLOW_VIDEO_GESTURES = "allow_video_gestures" +val TEMP_FOLDER_PATH = "temp_folder_path" +val VIEW_TYPE_FOLDERS = "view_type_folders" +val VIEW_TYPE_FILES = "view_type_files" +val SHOW_EXTENDED_DETAILS = "show_extended_details" +val EXTENDED_DETAILS = "extended_details" // slideshow val SLIDESHOW_INTERVAL = "slideshow_interval" val SLIDESHOW_INCLUDE_PHOTOS = "slideshow_include_photos" val SLIDESHOW_INCLUDE_VIDEOS = "slideshow_include_videos" +val SLIDESHOW_INCLUDE_GIFS = "slideshow_include_gifs" val SLIDESHOW_RANDOM_ORDER = "slideshow_random_order" val SLIDESHOW_USE_FADE = "slideshow_use_fade" val SLIDESHOW_MOVE_BACKWARDS = "slideshow_move_backwards" +val SLIDESHOW_LOOP = "loop_slideshow" val SLIDESHOW_DEFAULT_INTERVAL = 5 +val SLIDESHOW_SCROLL_DURATION = 500L val NOMEDIA = ".nomedia" @@ -53,15 +64,12 @@ val GET_ANY_INTENT = "get_any_intent" val SET_WALLPAPER_INTENT = "set_wallpaper_intent" val DIRECTORIES = "directories2" val IS_VIEW_INTENT = "is_view_intent" +val IS_FROM_GALLERY = "is_from_gallery" +val PICKED_PATHS = "picked_paths" val REQUEST_EDIT_IMAGE = 1 val REQUEST_SET_AS = 2 -// show media -val IMAGES_AND_VIDEOS = 0 -val IMAGES = 1 -val VIDEOS = 2 - // rotations val ROTATE_BY_SYSTEM_SETTING = 0 val ROTATE_BY_DEVICE_ROTATION = 1 @@ -70,4 +78,25 @@ val ROTATE_BY_ASPECT_RATIO = 2 val ORIENT_PORTRAIT = 0 val ORIENT_LANDSCAPE_LEFT = 1 val ORIENT_LANDSCAPE_RIGHT = 2 -val ORIENT_UPSIDE_DOWN = 3 + +// filter media +val IMAGES = 1 +val VIDEOS = 2 +val GIFS = 4 + +// view types +val VIEW_TYPE_GRID = 1 +val VIEW_TYPE_LIST = 2 + +// extended details values +val EXT_NAME = 1 +val EXT_PATH = 2 +val EXT_SIZE = 4 +val EXT_RESOLUTION = 8 +val EXT_LAST_MODIFIED = 16 +val EXT_DATE_TAKEN = 32 +val EXT_CAMERA_MODEL = 64 +val EXT_EXIF_PROPERTIES = 128 +val EXT_DURATION = 256 +val EXT_ARTIST = 512 +val EXT_ALBUM = 1024 diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/helpers/GlideDecoder.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/helpers/GlideDecoder.kt new file mode 100644 index 000000000..fbddb788b --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/helpers/GlideDecoder.kt @@ -0,0 +1,70 @@ +package com.simplemobiletools.gallery.helpers + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.media.ExifInterface +import android.net.Uri +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.Target +import com.davemorrissey.labs.subscaleview.decoder.ImageDecoder +import com.simplemobiletools.gallery.activities.ViewPagerActivity +import com.simplemobiletools.gallery.extensions.getFileSignature + +class GlideDecoder : ImageDecoder { + override fun decode(context: Context, uri: Uri): Bitmap { + val exif = android.media.ExifInterface(uri.path) + val orientation = exif.getAttributeInt(android.media.ExifInterface.TAG_ORIENTATION, android.media.ExifInterface.ORIENTATION_NORMAL) + + val targetWidth = if (ViewPagerActivity.screenWidth == 0) Target.SIZE_ORIGINAL else ViewPagerActivity.screenWidth + val targetHeight = if (ViewPagerActivity.screenHeight == 0) Target.SIZE_ORIGINAL else ViewPagerActivity.screenHeight + + val options = RequestOptions() + .signature(uri.path.getFileSignature()) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .override(targetWidth, targetHeight) + + val degrees = getRotationDegrees(orientation) + if (degrees != 0f) + options.transform(GlideRotateTransformation(context, getRotationDegrees(orientation))) + + val drawable = Glide.with(context) + .load(uri) + .apply(options) + .submit() + .get() + + return drawableToBitmap(drawable) + } + + private fun drawableToBitmap(drawable: Drawable): Bitmap { + if (drawable is BitmapDrawable) { + if (drawable.bitmap != null) { + return drawable.bitmap + } + } + + val bitmap = if (drawable.intrinsicWidth <= 0 || drawable.intrinsicHeight <= 0) { + Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + } else { + Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888) + } + + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + return bitmap + } + + // rotating backwards intentionally, as SubsamplingScaleImageView will rotate it properly at displaying + private fun getRotationDegrees(orientation: Int) = when (orientation) { + ExifInterface.ORIENTATION_ROTATE_270 -> 90f + ExifInterface.ORIENTATION_ROTATE_180 -> 180f + ExifInterface.ORIENTATION_ROTATE_90 -> 270f + else -> 0f + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/helpers/MediaFetcher.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/helpers/MediaFetcher.kt new file mode 100644 index 000000000..c42db1b4e --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/helpers/MediaFetcher.kt @@ -0,0 +1,331 @@ +package com.simplemobiletools.gallery.helpers + +import android.content.Context +import android.database.Cursor +import android.provider.MediaStore +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.helpers.SORT_BY_DATE_MODIFIED +import com.simplemobiletools.commons.helpers.SORT_BY_NAME +import com.simplemobiletools.commons.helpers.SORT_BY_SIZE +import com.simplemobiletools.commons.helpers.SORT_DESCENDING +import com.simplemobiletools.gallery.extensions.config +import com.simplemobiletools.gallery.extensions.containsNoMedia +import com.simplemobiletools.gallery.models.Medium +import java.io.File +import java.util.LinkedHashMap +import kotlin.collections.ArrayList +import kotlin.collections.component1 +import kotlin.collections.component2 + +class MediaFetcher(val context: Context) { + var shouldStop = false + + fun getMediaByDirectories(isPickVideo: Boolean, isPickImage: Boolean): HashMap> { + val media = getFilesFrom("", isPickImage, isPickVideo) + val excludedPaths = context.config.excludedFolders + val includedPaths = context.config.includedFolders + val showHidden = context.config.shouldShowHidden + val directories = groupDirectories(media) + + val removePaths = ArrayList() + for ((path, curMedia) in directories) { + // make sure the path has uppercase letters wherever appropriate + val groupPath = File(curMedia.first().path).parent + if (!File(groupPath).exists() || !shouldFolderBeVisible(groupPath, excludedPaths, includedPaths, showHidden)) { + removePaths.add(groupPath.toLowerCase()) + } + } + + removePaths.forEach { + directories.remove(it) + } + + return directories + } + + fun getFilesFrom(curPath: String, isPickImage: Boolean, isPickVideo: Boolean): ArrayList { + val projection = arrayOf(MediaStore.Images.Media._ID, + MediaStore.Images.Media.DISPLAY_NAME, + MediaStore.Images.Media.DATE_TAKEN, + MediaStore.Images.Media.DATE_MODIFIED, + MediaStore.Images.Media.DATA, + MediaStore.Images.Media.SIZE) + val uri = MediaStore.Files.getContentUri("external") + val selection = getSelectionQuery(curPath) + val selectionArgs = getSelectionArgsQuery(curPath) + + return try { + val cur = context.contentResolver.query(uri, projection, selection, selectionArgs, getSortingForFolder(curPath)) + parseCursor(context, cur, isPickImage, isPickVideo, curPath) + } catch (e: Exception) { + ArrayList() + } + } + + private fun getSelectionQuery(path: String): String? { + val dataQuery = "${MediaStore.Images.Media.DATA} LIKE ?" + return if (path.isEmpty()) { + if (context.isAndroidFour()) + return null + + var query = "($dataQuery)" + if (context.hasExternalSDCard()) { + query += " OR ($dataQuery)" + } + query + } else { + "($dataQuery AND ${MediaStore.Images.Media.DATA} NOT LIKE ?)" + } + } + + private fun getSelectionArgsQuery(path: String): Array? { + return if (path.isEmpty()) { + if (context.isAndroidFour()) + return null + + if (context.hasExternalSDCard()) { + arrayOf("${context.internalStoragePath}/%", "${context.sdCardPath}/%") + } else { + arrayOf("${context.internalStoragePath}/%") + } + } else { + arrayOf("$path/%", "$path/%/%") + } + } + + private fun parseCursor(context: Context, cur: Cursor, isPickImage: Boolean, isPickVideo: Boolean, curPath: String): ArrayList { + val curMedia = ArrayList() + val config = context.config + val filterMedia = config.filterMedia + val showHidden = config.shouldShowHidden + val includedFolders = config.includedFolders.map { "${it.trimEnd('/')}/" } + val excludedFolders = config.excludedFolders.map { "${it.trimEnd('/')}/" } + val noMediaFolders = getNoMediaFolders() + val isThirdPartyIntent = config.isThirdPartyIntent + + cur.use { + if (cur.moveToFirst()) { + do { + try { + if (shouldStop) + break + + val path = cur.getStringValue(MediaStore.Images.Media.DATA).trim() + var filename = cur.getStringValue(MediaStore.Images.Media.DISPLAY_NAME)?.trim() ?: "" + if (filename.isEmpty()) + filename = path.getFilenameFromPath() + + val isImage = filename.isImageFast() + val isVideo = if (isImage) false else filename.isVideoFast() + val isGif = if (isImage || isVideo) false else filename.isGif() + + if (!isImage && !isVideo && !isGif) + continue + + if (isVideo && (isPickImage || filterMedia and VIDEOS == 0)) + continue + + if (isImage && (isPickVideo || filterMedia and IMAGES == 0)) + continue + + if (isGif && filterMedia and GIFS == 0) + continue + + if (!showHidden && filename.startsWith('.')) + continue + + var size = cur.getLongValue(MediaStore.Images.Media.SIZE) + val file = File(path) + if (size == 0L) { + size = file.length() + } + + if (size <= 0L) + continue + + var isExcluded = false + excludedFolders.forEach { + if (path.startsWith(it)) { + isExcluded = true + includedFolders.forEach { + if (path.startsWith(it)) { + isExcluded = false + } + } + } + } + + if (!isExcluded && !showHidden) { + noMediaFolders.forEach { + if (path.startsWith(it)) { + isExcluded = true + } + } + } + + if (!isExcluded && !showHidden && path.contains("/.")) { + isExcluded = true + } + + if (!isExcluded || isThirdPartyIntent) { + if (!file.exists()) + continue + + val dateTaken = cur.getLongValue(MediaStore.Images.Media.DATE_TAKEN) + val dateModified = cur.getIntValue(MediaStore.Images.Media.DATE_MODIFIED) * 1000L + + val medium = Medium(filename, path, isVideo, dateModified, dateTaken, size) + curMedia.add(medium) + } + } catch (e: Exception) { + continue + } + } while (cur.moveToNext()) + } + } + + config.includedFolders.filter { it.isNotEmpty() && (curPath.isEmpty() || it == curPath) }.forEach { + getMediaInFolder(it, curMedia, isPickImage, isPickVideo, filterMedia) + } + + if (isThirdPartyIntent && curPath.isNotEmpty() && curMedia.isEmpty()) { + getMediaInFolder(curPath, curMedia, isPickImage, isPickVideo, filterMedia) + } + + Medium.sorting = config.getFileSorting(curPath) + curMedia.sort() + + return curMedia + } + + private fun groupDirectories(media: ArrayList): HashMap> { + val directories = LinkedHashMap>() + for (medium in media) { + if (shouldStop) + break + + val parentDir = File(medium.path).parent?.toLowerCase() ?: continue + if (directories.containsKey(parentDir)) { + directories[parentDir]!!.add(medium) + } else { + directories.put(parentDir, arrayListOf(medium)) + } + } + return directories + } + + private fun shouldFolderBeVisible(path: String, excludedPaths: MutableSet, includedPaths: MutableSet, showHidden: Boolean): Boolean { + val file = File(path) + return if (includedPaths.contains(path)) { + true + } else if (isThisOrParentExcluded(path, excludedPaths, includedPaths)) { + false + } else if (!showHidden && file.isDirectory && file.canonicalFile == file.absoluteFile) { + var containsNoMediaOrDot = file.containsNoMedia() || path.contains("/.") + if (!containsNoMediaOrDot) { + containsNoMediaOrDot = checkParentHasNoMedia(file.parentFile) + } + !containsNoMediaOrDot + } else { + true + } + } + + private fun checkParentHasNoMedia(file: File): Boolean { + var curFile = file + while (true) { + if (curFile.containsNoMedia()) { + return true + } + curFile = curFile.parentFile + if (curFile.absolutePath == "/") + break + } + return false + } + + private fun isThisOrParentExcluded(path: String, excludedPaths: MutableSet, includedPaths: MutableSet) = + includedPaths.none { path.startsWith(it) } && excludedPaths.any { path.startsWith(it) } + + + private fun getMediaInFolder(folder: String, curMedia: ArrayList, isPickImage: Boolean, isPickVideo: Boolean, filterMedia: Int) { + val files = File(folder).listFiles() ?: return + for (file in files) { + if (shouldStop) + break + + val filename = file.name + val isImage = filename.isImageFast() + val isVideo = if (isImage) false else filename.isVideoFast() + val isGif = if (isImage || isVideo) false else filename.isGif() + + if (!isImage && !isVideo) + continue + + if (isVideo && (isPickImage || filterMedia and VIDEOS == 0)) + continue + + if (isImage && (isPickVideo || filterMedia and IMAGES == 0)) + continue + + if (isGif && filterMedia and GIFS == 0) + continue + + val size = file.length() + if (size <= 0L) + continue + + val dateTaken = file.lastModified() + val dateModified = file.lastModified() + + val medium = Medium(filename, file.absolutePath, isVideo, dateModified, dateTaken, size) + val isAlreadyAdded = curMedia.any { it.path == file.absolutePath } + if (!isAlreadyAdded) { + curMedia.add(medium) + context.scanPath(file.absolutePath) {} + } + } + } + + private fun getSortingForFolder(path: String): String { + val sorting = context.config.getFileSorting(path) + val sortValue = when { + sorting and SORT_BY_NAME > 0 -> MediaStore.Images.Media.DISPLAY_NAME + sorting and SORT_BY_SIZE > 0 -> MediaStore.Images.Media.SIZE + sorting and SORT_BY_DATE_MODIFIED > 0 -> MediaStore.Images.Media.DATE_MODIFIED + else -> MediaStore.Images.Media.DATE_TAKEN + } + + return if (sorting and SORT_DESCENDING > 0) + "$sortValue DESC" + else + "$sortValue ASC" + } + + private fun getNoMediaFolders(): ArrayList { + val folders = ArrayList() + val noMediaCondition = "${MediaStore.Files.FileColumns.MEDIA_TYPE} = ${MediaStore.Files.FileColumns.MEDIA_TYPE_NONE}" + + val uri = MediaStore.Files.getContentUri("external") + val columns = arrayOf(MediaStore.Files.FileColumns.DATA) + val where = "$noMediaCondition AND ${MediaStore.Files.FileColumns.TITLE} LIKE ?" + val args = arrayOf("%$NOMEDIA%") + var cursor: Cursor? = null + + try { + cursor = context.contentResolver.query(uri, columns, where, args, null) + if (cursor?.moveToFirst() == true) { + do { + val path = cursor.getString(cursor.getColumnIndex(MediaStore.Files.FileColumns.DATA)) ?: continue + val noMediaFile = File(path) + if (noMediaFile.exists()) + folders.add("${noMediaFile.parent}/") + } while (cursor.moveToNext()) + } + } finally { + cursor?.close() + } + + return folders + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/models/Directory.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/models/Directory.kt index 6293eba76..d03b4d1be 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/models/Directory.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/models/Directory.kt @@ -4,41 +4,32 @@ import com.simplemobiletools.commons.helpers.* import java.io.Serializable data class Directory(val path: String, val tmb: String, val name: String, var mediaCnt: Int, val modified: Long, val taken: Long, - var size: Long) : Serializable, Comparable { + val size: Long) : Serializable, Comparable { companion object { private val serialVersionUID = -6553345863555455L var sorting: Int = 0 } - fun addSize(bytes: Long) { - size += bytes - } - override fun compareTo(other: Directory): Int { var result: Int - if (sorting and SORT_BY_NAME != 0) { - result = AlphanumComparator().compare(name.toLowerCase(), other.name.toLowerCase()) - } else if (sorting and SORT_BY_SIZE != 0) { - result = if (size == other.size) - 0 - else if (size > other.size) - 1 - else - -1 - } else if (sorting and SORT_BY_DATE_MODIFIED != 0) { - result = if (modified == other.modified) - 0 - else if (modified > other.modified) - 1 - else - -1 - } else { - result = if (taken == other.taken) - 0 - else if (taken > other.taken) - 1 - else - -1 + when { + sorting and SORT_BY_NAME != 0 -> result = AlphanumericComparator().compare(name.toLowerCase(), other.name.toLowerCase()) + sorting and SORT_BY_PATH != 0 -> result = AlphanumericComparator().compare(path.toLowerCase(), other.path.toLowerCase()) + sorting and SORT_BY_SIZE != 0 -> result = when { + size == other.size -> 0 + size > other.size -> 1 + else -> -1 + } + sorting and SORT_BY_DATE_MODIFIED != 0 -> result = when { + modified == other.modified -> 0 + modified > other.modified -> 1 + else -> -1 + } + else -> result = when { + taken == other.taken -> 0 + taken > other.taken -> 1 + else -> -1 + } } if (sorting and SORT_DESCENDING != 0) { diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/models/Medium.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/models/Medium.kt index 1663c60b1..fadf715ea 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/models/Medium.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/models/Medium.kt @@ -25,29 +25,23 @@ data class Medium(var name: String, var path: String, val video: Boolean, val mo override fun compareTo(other: Medium): Int { var result: Int - if (sorting and SORT_BY_NAME != 0) { - result = AlphanumComparator().compare(name.toLowerCase(), other.name.toLowerCase()) - } else if (sorting and SORT_BY_SIZE != 0) { - result = if (size == other.size) - 0 - else if (size > other.size) - 1 - else - -1 - } else if (sorting and SORT_BY_DATE_MODIFIED != 0) { - result = if (modified == other.modified) - 0 - else if (modified > other.modified) - 1 - else - -1 - } else { - result = if (taken == other.taken) - 0 - else if (taken > other.taken) - 1 - else - -1 + when { + sorting and SORT_BY_NAME != 0 -> result = AlphanumericComparator().compare(name.toLowerCase(), other.name.toLowerCase()) + sorting and SORT_BY_SIZE != 0 -> result = when { + size == other.size -> 0 + size > other.size -> 1 + else -> -1 + } + sorting and SORT_BY_DATE_MODIFIED != 0 -> result = when { + modified == other.modified -> 0 + modified > other.modified -> 1 + else -> -1 + } + else -> result = when { + taken == other.taken -> 0 + taken > other.taken -> 1 + else -> -1 + } } if (sorting and SORT_DESCENDING != 0) { diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/views/MySquareImageView.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/views/MySquareImageView.kt index 3e0c79df1..f254d4183 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/views/MySquareImageView.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/views/MySquareImageView.kt @@ -14,8 +14,7 @@ class MySquareImageView : ImageView { constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - val spec = if (isVerticalScrolling) measuredWidth else measuredHeight - setMeasuredDimension(spec, spec) + val spec = if (isVerticalScrolling) widthMeasureSpec else heightMeasureSpec + super.onMeasure(spec, spec) } } diff --git a/app/src/main/res/drawable-hdpi/img_play_outline_empty.png b/app/src/main/res/drawable-hdpi/img_play_outline_empty.png new file mode 100644 index 000000000..6212066d5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/img_play_outline_empty.png differ diff --git a/app/src/main/res/drawable-xhdpi/img_play_outline_empty.png b/app/src/main/res/drawable-xhdpi/img_play_outline_empty.png new file mode 100644 index 000000000..2753608b6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/img_play_outline_empty.png differ diff --git a/app/src/main/res/drawable-xxhdpi/img_play_outline_empty.png b/app/src/main/res/drawable-xxhdpi/img_play_outline_empty.png new file mode 100644 index 000000000..95a4f7d28 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/img_play_outline_empty.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/img_play_outline_empty.png b/app/src/main/res/drawable-xxxhdpi/img_play_outline_empty.png new file mode 100644 index 000000000..df8727abd Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/img_play_outline_empty.png differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 19fd7e7bd..9432206a7 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -11,6 +11,28 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - diff --git a/app/src/main/res/layout/dialog_change_sorting.xml b/app/src/main/res/layout/dialog_change_sorting.xml index d0ab9b36f..e0e642acc 100644 --- a/app/src/main/res/layout/dialog_change_sorting.xml +++ b/app/src/main/res/layout/dialog_change_sorting.xml @@ -28,6 +28,14 @@ android:paddingTop="@dimen/medium_margin" android:text="@string/name"/> + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_directory_picker.xml b/app/src/main/res/layout/dialog_directory_picker.xml index 0a586affb..bc18bfb19 100644 --- a/app/src/main/res/layout/dialog_directory_picker.xml +++ b/app/src/main/res/layout/dialog_directory_picker.xml @@ -10,7 +10,7 @@ diff --git a/app/src/main/res/layout/dialog_filter_media.xml b/app/src/main/res/layout/dialog_filter_media.xml new file mode 100644 index 000000000..37f2aba11 --- /dev/null +++ b/app/src/main/res/layout/dialog_filter_media.xml @@ -0,0 +1,36 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_manage_extended_details.xml b/app/src/main/res/layout/dialog_manage_extended_details.xml new file mode 100644 index 000000000..a31a684a6 --- /dev/null +++ b/app/src/main/res/layout/dialog_manage_extended_details.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_medium_picker.xml b/app/src/main/res/layout/dialog_medium_picker.xml index 60207b4c9..f3c41d8f0 100644 --- a/app/src/main/res/layout/dialog_medium_picker.xml +++ b/app/src/main/res/layout/dialog_medium_picker.xml @@ -1,10 +1,54 @@ - - + android:paddingTop="@dimen/activity_margin"> + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_slideshow.xml b/app/src/main/res/layout/dialog_slideshow.xml index d057acf77..dd17378d4 100644 --- a/app/src/main/res/layout/dialog_slideshow.xml +++ b/app/src/main/res/layout/dialog_slideshow.xml @@ -78,7 +78,7 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/directory_item.xml b/app/src/main/res/layout/directory_item_grid.xml similarity index 95% rename from app/src/main/res/layout/directory_item.xml rename to app/src/main/res/layout/directory_item_grid.xml index f6d296e07..6903b7f46 100644 --- a/app/src/main/res/layout/directory_item.xml +++ b/app/src/main/res/layout/directory_item_grid.xml @@ -39,7 +39,9 @@ android:id="@+id/dir_shadow_holder" android:layout_width="match_parent" android:layout_height="@dimen/tmb_shadow_height" + android:layout_alignLeft="@+id/dir_bottom_holder" android:layout_alignParentBottom="true" + android:layout_alignRight="@+id/dir_bottom_holder" android:background="@drawable/gradient_background"/> + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/pager_photo_item.xml b/app/src/main/res/layout/pager_photo_item.xml index 1c4d2d76a..e48b6b90a 100644 --- a/app/src/main/res/layout/pager_photo_item.xml +++ b/app/src/main/res/layout/pager_photo_item.xml @@ -1,6 +1,7 @@ @@ -16,4 +17,17 @@ android:layout_height="match_parent" android:visibility="gone"/> + + diff --git a/app/src/main/res/layout/pager_video_item.xml b/app/src/main/res/layout/pager_video_item.xml index 3d88f52f7..d0be9b3d2 100644 --- a/app/src/main/res/layout/pager_video_item.xml +++ b/app/src/main/res/layout/pager_video_item.xml @@ -1,6 +1,7 @@ @@ -50,6 +51,19 @@ android:textColor="@android:color/white" android:textSize="@dimen/extra_big_text_size"/> + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/cab_media.xml b/app/src/main/res/menu/cab_media.xml index 4b47e6025..fa787d520 100644 --- a/app/src/main/res/menu/cab_media.xml +++ b/app/src/main/res/menu/cab_media.xml @@ -1,6 +1,11 @@ + + + + + + + + Simple Gallery + Gallery + Edit + Open camera + Open with + No valid app found + (hidden) + Pin folder + Unpin folder + Show all folders content + All folders + Switch to folder view + Other folder + Show on map + Unknown location + No application with maps has been found + No Camera app has been found + Increase column count + Reduce column count + Change cover image + Select photo + Use default + Set as + Volume + Brightness + Do not ask again in this session + + + Filter media + Images + Videos + GIFs + No media files have been found with the selected filters. + Change filters + + + This function hides the folder by adding a \'.nomedia\' file into it, it will hide all subfolders too. You can see them by toggling the \'Show hidden folders\' option in Settings. Continue? + Exclude + Excluded folders + Manage excluded folders + This will exclude the selection together with its subfolders from Simple Gallery only. You can manage excluded folders in Settings. + Exclude a parent instead? + Excluding folders will make them together with their subfolders hidden just in Simple Gallery, they will still be visible in other applications.\n\nIf you want to hide them from other apps too, use the Hide function. + Remove all + Remove all folders from the list of excluded? This will not delete the folders. + + + Included folders + Manage included folders + Add folder + If you have some folders which contain media, but were not recognized by the app, you can add them manually here.\n\nAdding some items here will not exclude any other folder. + + + Resize + Resize selection and save + Width + Height + Keep aspect ratio + Please enter a valid resolution + + + Editor + Save + Rotate + Path + Invalid image path + Image editing failed + Edit image with: + No image editor found + Unknown file location + Could not overwrite the source file + Rotate left + Rotate right + Rotate by 180º + Flip + Flip horizontally + Flip vertically + Edit with + + + Simple Wallpaper + Set as Wallpaper + Setting as Wallpaper failed + Set as wallpaper with: + No app capable of it has been found + Setting wallpaper… + Wallpaper set successfully + Portrait aspect ratio + Landscape aspect ratio + + + Slideshow + Interval (seconds): + Include photos + Include videos + Include GIFs + Random order + Use fade animations + Move backwards + Loop slideshow + The slideshow ended + No media for the slideshow have been found + + + Change view type + Grid + List + + + Show hidden media + Play videos automatically + Toggle filename visibility + Loop videos + Animate GIFs at thumbnails + Max brightness when viewing media + Crop thumbnails into squares + Rotate fullscreen media by + System setting + Device rotation + Aspect ratio + Dark background at fullscreen media + Scroll thumbnails horizontally + Automatically hide system UI at fullscreen media + Delete empty folders after deleting their content + Allow controlling video volume and brightness with vertical gestures + Replace Share with Rotate at fullscreen menu + Show extended details over fullscreen media + Manage extended details + + + + A gallery for viewing photos and videos without ads. + + A simple tool usable for viewing photos and videos. Items can be sorted by date, size, name both ascending or descending, photos can be zoomed in. Media files are shown in multiple columns depending on the size of the display, you can change the column count by pinch gestures. They can be renamed, shared, deleted, copied, moved. Images can also be cropped, rotated, flipped or set as Wallpaper directly from the app. + + The Gallery is also offered for third party usage for previewing images / videos, adding attachments at email clients etc. It\'s perfect for everyday usage. + + Contains no ads or unnecessary permissions. It is fully opensource, provides customizable colors. + + This app is just one piece of a bigger series of apps. You can find the rest of them at http://www.simplemobiletools.com + + + + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 3b88dc92d..c6ba60327 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -25,6 +25,15 @@ Nastavit jako Volume Brightness + Do not ask again in this session + + + Filter media + Images + Videos + GIFs + No media files have been found with the selected filters. + Change filters Tato funkce skryje složku, včetně podsložek, přidáním souboru \'.nomedia\'. Zobrazíte je zvolením možnosti \'Zobrazit skryté složky\' v nastavení. Pokračovat? @@ -84,24 +93,25 @@ Slideshow Interval (seconds): - Include photos and GIFs + Include photos Include videos + Include GIFs Random order Use fade animations Move backwards + Loop slideshow The slideshow ended No media for the slideshow have been found + + Change view type + Grid + List + Zobrazit skryté média Automaticky přehrávat videa Přepnout viditelnost názvů souborů - Zobrazit média - Jen obrázky - Jen videa - GIFs only - Images, videos, GIFs - Obrázky i videa Přehrávat videa ve smyčce Animovat náhledy souborů GIF Nastavit jas obrazovky na max při zobrazení médií @@ -113,7 +123,11 @@ Dark background at fullscreen media Scroll thumbnails horizontally Automatically hide system UI at fullscreen media + Delete empty folders after deleting their content + Allow controlling video volume and brightness with vertical gestures Replace Share with Rotate at fullscreen menu + Show extended details over fullscreen media + Manage extended details diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ba4a39d71..3a524eca2 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -19,19 +19,28 @@ Keine Kamera-App gefunden Kacheln verkleinern Kacheln vergrößern - Cover-Bild ändern + Coverbild ändern Auswählen Standard Festlegen als Lautstärke Helligkeit + Nicht erneut fragen (in dieser Session) + + + Filter + Bilder + Videos + GIFs + Keine Medien für die ausgewählten Filter gefunden + Filter ändern - Diese Funktion versteckt ausgewählte Ordner (auch für andere Apps), indem dort im Dateisystem eine \'.nomedia\'-Datei abgelegt wird. Dadurch werden auch deren Unterordner versteckt. Solche Ordner werden nur gezeigt, wenn die Einstellung \'Versteckte Ordner zeigen\' aktiv ist (auch andere Apps bieten üblicherweise eine solche Option). Fortfahren? + Diese Funktion versteckt die ausgewählten Ordner (auch für andere Apps), indem dort im Dateisystem eine \'.nomedia\'-Datei abgelegt wird. Dadurch werden auch deren Unterordner versteckt. Solche Ordner werden nur gezeigt, wenn die Einstellung \'Versteckte Ordner zeigen\' aktiv ist (auch andere Apps bieten üblicherweise eine solche Option). Fortfahren? Ordner ausblenden Ausgeblendete Ordner Ausgeblendete Ordner verwalten - Diese Funktion blendet ausgewählte Ordner und deren Unterordner aus (nur in dieser App). Ausgeblendete Ordner können in den Einstellungen verwaltet werden. + Diese Funktion blendet die ausgewählten Ordner und deren Unterordner aus (nur in dieser App). Ausgeblendete Ordner können in den Einstellungen verwaltet werden. Möchten Sie stattdessen einen höherliegenden Ordner ausblenden? \'Ordner ausblenden\' wird ausgewählte Ordner und deren Unterordner nur in dieser App ausblenden. Andere Apps werden solche Ordner weiterhin anzeigen.\\n\\nWenn Sie Ordner auch für andere Apps verstecken wollen, verwenden Sie dafür die Funktion \'Ordner verstecken\'. Alle entfernen @@ -84,24 +93,25 @@ Diashow Intervall (Sekunden): - Bilder/GIFs verwenden + Bilder verwenden Videos verwenden + GIFs verwenden Zufällige Reihenfolge Übergänge animieren Rückwärts abspielen + Endlos abspielen Diashow beendet Keine Medien für Diashow gefunden + + Darstellung ändern + Gitter + Liste + Versteckte Ordner zeigen Videos automatisch abspielen Beschriftungen ein/aus - Medien auswählen - Nur Bilder - Nur Videos - Nur GIFs - Bilder, Videos und GIFs - Bilder und Videos Videos in Endlosschleife spielen Kacheln von GIFs animieren Helligkeit beim Betrachten maximieren @@ -113,7 +123,11 @@ Schwarzer Hintergrund im Vollbild Kacheln horizontal scrollen Systemleisten ausblenden im Vollbild + Nach Löschen leere Ordner löschen + Gesten für Videolautstärke/Helligkeit Teilen/Drehen im Vollbild-Menü vertauschen + Eigenschaften anzeigen im Vollbild + Eigenschaften auswählen diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index b5bc78a3a..8a364e187 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -25,6 +25,15 @@ Establecer como Volume Brightness + Do not ask again in this session + + + Filter media + Images + Videos + GIFs + No media files have been found with the selected filters. + Change filters Esta función oculta la carpeta agregando un archivo \'.nomedia\' en ella, y ocultará también las subcarpetas. Puede mostrarlas cambiando la opción \'Mostrar carpetas ocultas\' en los Ajustes. ¿Continuar? @@ -84,24 +93,25 @@ Slideshow Interval (seconds): - Include photos and GIFs + Include photos Include videos + Include GIFs Random order Use fade animations Move backwards + Loop slideshow The slideshow ended No media for the slideshow have been found + + Change view type + Grid + List + Mostrar carpetas ocultas Reproducir vídeos automáticamente Cambiar la visibilidad del nombre de archivo - Mostrar multimedia - Solo imágenes - Solo vídeos - solo GIFs - Imágenes, vídeos, GIFs - Imágenes y vídeos Reproducción continua de vídeos Animar las miniaturas de GIFs Brillo máximo cuando se muestra multimedia @@ -113,7 +123,11 @@ Utilizar siempre fondo oscuro en pantalla completa Desplazar miniaturas horizontalmente Ocultar automáticamente la interfaz de usuario del sistema en medios de pantalla completa + Delete empty folders after deleting their content + Allow controlling video volume and brightness with vertical gestures Reemplazar Compartir con Girar en el menú de pantalla completa + Show extended details over fullscreen media + Manage extended details diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml new file mode 100644 index 000000000..a4edf365a --- /dev/null +++ b/app/src/main/res/values-fi/strings.xml @@ -0,0 +1,149 @@ + + + Simple Gallery + Galleria + Muokkaa + Avaa kamera + Avaa + Sovelluksia ei löydetty + (piilotettu) + Kiinnitä kansio + Poista kiinnitys + Näytä kaikkien kansioiden sisältö + Kaikki kansiot + Vaihda kansionäkymään + Muu kansio + Näytä kartalla + Tuntematon sijainti + Karttasovellusta ei löytynyt + Kamerasovellusta ei löytynyt + Increase column count + Reduce column count + Vaihda kansikuva + Valitse kuva + Käytä oletuksia + Aseta + Äänenvoimakkuus + Kirkkaus + Do not ask again in this session + + + Suodata media + Kuvat + Videot + GIFit + Mediaa ei löytynyt valituilla suotimilla. + Muuta suotimia + + + Tämä piilottaa kansion ja alikansiot lisäämällä \'.nomedia\' tiedoston kansioon. Näet ne valitsemalla \'Näytä piilotetut kansiot\' asetuksissa. Continue? + Poissulje + Poissuljetut kansiot + Hallitse poissuljettuja kansioita + Tämä poissulkee valitun ja alikansiot vain Simple Gallerysta. Voit hallinnoida poissuljettuja kansioita asetuksista. + Poissulje yläkansio tämän sijaan? + Kansion poissulkeminen piilottaa kansion alikansioineen vain Simple Galleryssa, ne jäävät näkyviin muihin sovelluksiin.\n\nJos haluat piilottaa kansion myös muissa sovelluksissa, käytä piilota-funktiota. + Poista kaikki + Poista kaikki kansiot poissuljettujen listasta? Tämä ei poista kansioita. + + + Sisällytä kansiot + Hallitse sisällettyjä kansioita + Lisää kansio + Jos sinulla on kansioita, jotka sisältää mediaa, mutta sovellus ei tunnistanut, voit lisätä ne manuaalisesti tähän.\n\Lisääminen ei poissulje muita kansioita. + + + Rajaa + Rajaa valinta ja tallenna + Leveys + Korkeus + Säilytä kuvasuhde + Aseta oikea resoluutio. + + + Editori + Tallenna + Käännä + Polku + Kuvan polkua ei ole + Kuvan muokkaus epäonnistui + Muokkaa kuvaa: + Kuvamuokkainta ei löytynyt + Tuntematon tiedostosijainti + Lähdetiedoston ylikirjoitus epäonnistui + Käännä vasemmalle + Käännä oikealle + Käännä 180º + Pyöräytä + Pyöräytä vaakasuoraan + Pyöräytä pystysuoraan + Muokkaa sovelluksella + + + Simple Wallpaper + Aseta taustakuvaksi + Taustakuvan asetus epäonnistui + Aseta taustakuvaksi sovelluksella: + Toimivaa sovellusta ei löydetty + Asetetaan taustakuvaa… + Taustakuva asetettu onnistuneesti + Kuvasuhde pystyssä + Kuvasuhde vaakatasossa + + + Diaesitys + Aikaväli (sekunteja): + Sisällytä Kuvat + Sisällytä Videot + Sisällytä GIFit + Satunnainen järjestys + Käytä häivitys-animaatiota + Liiku takaisinpäin + Jatkuva diaesitys + Diaesitys päättyi + Mediaa diaesitykseen ei löytynyt + + + Vaihda näkymää + Ruudukko + Lista + + + Näytä piilotettu media + Toista videot automaattisesti + Tiedostonimien näkyvyys + Jatkuvat videot + Animoi GIFit pienoiskuvissa + Täysi kirkkaus mediaa katsoessa + Leikkaa pienoiskuvat neliöiksi + Käännä koko ruudun mediaa + Järjestelmän asetukset + Laitteen kierto + Kuvasuhde + Tumma tausta koko ruudun medioissa + Vieritä pienoiskuvia vaakasuorassa + Piilota järjestelmän UI automaattisesti koko näytön mediassa + Poista tyhjät kansiot kansion tyhjennyksen jälkeen + Salli videon äänenvoimakkuuden ja kirkkauden säätö pystysuorilla eleillä + Korvaa jakaminen kääntämisellä koko näytön tilassa + Show extended details over fullscreen media + Manage extended details + + + + Galleria kuvien ja videoiden katsomiseen ilman mainoksia. + + Yksinkertainen työkalu kuvien ja videoiden katsomiseen. Kohteita voidaan lajitella päivän, koon, nimen mukaan, nousevassa ja laskevassa järjestyksessä. Kuvia voidaan zoomata. Mediatiedostot näkyvät useissa sarakkeissa joiden määrää muutetaan nipistys-eleellä. Tiedostoja voidaan uudelleennimetä, jakaa, poistaa, kopioida. Kuvia voi rajata, pyörittää tai asettaa taustakuvaksi suoraan sovelluksesta. + + Galleriaa tarjotaan myös kolmansille osapuolille kuvien / videoiden tarkasteluun, liitteiden lisäämiseksi sähköpostiin yms. Täydellinen jokapäiväiseen käyttöön. + + Ei sisällä mainoksia tai turhia käyttöoikeuksia. Täysin avointa lähdekoodia, tarjoaa muokattavat värit. + + Tämä sovellus on vain yksi osa suurempaa kokoelmaa. Löydät loput osoitteesta http://www.simplemobiletools.com + + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 09c8d563c..4fd96a948 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -7,7 +7,7 @@ Ouvrir avec Aucune application valide trouvée (caché) - Épingler les dossiers + Épingler le dossier Désépingler le dossier Afficher le contenu de tous les dossiers Tous les dossiers @@ -15,16 +15,25 @@ Autre dossier Afficher sur la carte Position inconnue - Aucune application de cartes n\'a été trouvée - Aucune application d\\appareil photo n\'a été trouvée + Aucune application de carte n\'a été trouvée + Aucune application d\'appareil photo n\'a été trouvée Augmenter le nombre de colonnes Réduire le nombre de colonnes - Change cover image + Changer l\'image de couverture Sélectionner une photo - Use default - Set as + Utiliser par défaut + Définir comme Volume Luminosité + Ne pas redemander pour cette session + + + Filtrer les médias + Images + Vidéos + GIF + Aucun fichier média trouvé avec les filtres sélectionnés. + Changer les filtres Cette option masque le dossier en ajoutant un fichier \'.nomedia\' à l\'intérieur, cela masquera aussi tous les sous-dossiers. Vous pouvez les voir en modifiant l\'option \'Afficher les dossiers cachés\' dans les Paramètres. Continuer ? @@ -41,7 +50,7 @@ Dossiers inclus Gérer les dossiers inclus Ajouter un dossier - Si vous avez des dossiers contenant du media et qui ne sont pas reconnus par l\'application alors, vous pouvez les ajouter manuellement ici. + Si vous avez des dossiers contenant des médias et qui ne sont pas reconnus par l\'application alors, vous pouvez les ajouter manuellement ici. Redimensionner @@ -49,7 +58,7 @@ Largeur Hauteur Garder le ratio - Veuillez entrez une résolution valide + Veuillez entrer une résolution valide Éditeur @@ -78,47 +87,60 @@ Aucune application trouvée pour continuer cette action Paramètres de fond d\'écran… Fond d\'écran défini avec succès - Ratio aspect Portrait - Ratio aspect Paysage + Ratio d\'aspect portrait + Ratio d\'aspect paysage Diaporama - Intervalle​ (secondes): - Inclure photos et GIFs + Intervalle (secondes) : + Inclure photos Inclure vidéos + Inclure GIF Ordre aléatoire Utiliser un fondu - Move backwards + Revenir en arrière + Diaporama en boucle Diaporama terminé - No media for the slideshow have been found + Aucun média trouvé pour le diaporama + + + Changer le type de vue + Grille + Liste Afficher les dossiers cachés Lecture automatique des vidéos Permuter la visibilité des noms de fichier - Afficher les médias - Seulement les images - Vidéos uniquement - GIFs seulement - Images, vidéos, GIFs - Images et vidéos - Tourner en boucle les vidéos - GIFs animés sur les miniatures + Lire en boucle les vidéos + GIF animés sur les miniatures Luminosité maximale lors de l\'affichage de media Rogner les miniatures en carrés Pivoter les medias plein écran selon Paramètres système Rotation de l\'appareil Ratio d\'aspect - Dark background at fullscreen media - Défilement des mignatures horizontalement - Automatically hide system UI at fullscreen media - Replace Share with Rotate at fullscreen menu + Arrière-plan sombre pour média plein écran + Défilement des miniatures horizontalement + Masquer automatiquement l\'interface utilisateur si média plein écran + Supprimer les dossiers vides après avoir supprimé leur contenu + Permettre le contrôle du volume vidéo et de la luminosité avec des gestes verticaux + Remplacer Partager par Pivoter si menu en plein écran + Afficher les détails supplémentaires par dessus le média en plein écran + Gérer les détails supplémentaires Un album pour visionner photos et vidéos sans publicité. - Un simple outil pour visionner les photos et les vidéos. Elles peuvent être triées par dates, tailles, noms dans les deux sens (alphabétique comme désalphabétique), il est possible de zoomer sur les photos. Les fichiers sont affichés sur de multiple colonnes en fonction de la taille de l\'écran, vous pouvez changer le nombre de colonnes par pincement. Elles peuvent être renommées, partagées, supprimées, copiées et déplacées. Les images peuvent en plus être tournées, rognées ou être définies comme fond d\'écran directement depuis l\'application. La galerie est aussi offerte pour l\'utiliser comme une tierce partie pour de la prévisualisation des images/vidéos, joindre aux clients mail etc. C\'est parfait pour un usage au quotidien. L\'application ne contient ni de publicité ni d\'autorisation inutile. Elle est totalement OpenSource et est aussi fournie avec un thème sombre. Cette application est juste l\'une des applications d\'une plus grande suite. Vous pouvez trouver les autres sur http://www.simplemobiletools.com + + Un simple outil pour visionner les photos et les vidéos. Elles peuvent être triées par dates, tailles, noms dans les deux sens (alphabétique comme désalphabétique), il est possible de zoomer sur les photos. Les fichiers sont affichés sur de multiple colonnes en fonction de la taille de l\'écran, vous pouvez changer le nombre de colonnes par pincement. Elles peuvent être renommées, partagées, supprimées, copiées et déplacées. Les images peuvent en plus être tournées, rognées ou être définies comme fond d\'écran directement depuis l\'application. + + La galerie est également proposée pour une utilisation comme tierce partie pour la prévisualisation des images/vidéos, ajouter des pièces jointes aux clients email etc. C\'est parfait pour un usage au quotidien. + + L\'application ne contient ni publicité ni autorisation inutile. Elle est totalement opensource et est aussi fournie avec des couleurs personnalisables. + + Cette application est juste l\'une des applications d\'une plus grande suite. Vous pouvez trouver les autres sur http://www.simplemobiletools.com + + Filter media + Images + Videos + GIFs + No media files have been found with the selected filters. + Change filters This function hides the folder by adding a \'.nomedia\' file into it, it will hide all subfolders too. You can see them by toggling the \'Show hidden folders\' option in Settings. Continue? @@ -41,7 +50,7 @@ Included folders Manage included folders Add folder - If you have some folders which contain media, but were not recognized by the app, you can add them manually here. + If you have some folders which contain media, but were not recognized by the app, you can add them manually here.\n\nAdding some items here will not exclude any other folder. Resize @@ -84,24 +93,25 @@ Slideshow Interval (seconds): - Include photos and GIFs + Include photos Include videos + Include GIFs Random order Use fade animations Move backwards + Loop slideshow The slideshow ended No media for the slideshow have been found + + Change view type + Grid + List + Show hidden media Play videos automatically Toggle filename visibility - Show media - Images only - Videos only - GIFs only - Images, videos, GIFs - Images and videos Loop videos Animate GIFs at thumbnails Max brightness when viewing media @@ -113,7 +123,11 @@ Dark background at fullscreen media Scroll thumbnails horizontally Automatically hide system UI at fullscreen media + Delete empty folders after deleting their content + Allow controlling video volume and brightness with vertical gestures Replace Share with Rotate at fullscreen menu + Show extended details over fullscreen media + Manage extended details diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b70a13b6c..2b187ae34 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -25,6 +25,15 @@ Imposta come Volume Luminosità + Do not ask again in this session + + + Filtra i media + Immagini + Video + GIF + Nessun file trovato per il filtro selezionato. + Cambia filtro Questa funzione nasconde la cartella aggiungendo un file \'.nomedia\' all\'interno, nasconderà anche tutte le sottocartelle. Puoi vederle attivando l\'opzione \'Mostra cartelle nascoste\' nelle impostazioni. Continuare? @@ -84,24 +93,25 @@ Presentazione Intervallo (secondi): - Includi foto e GIF + Includi foto Includi video + Includi GIF Ordine sparso Usa animazioni a dissolvenza Scorri al contrario + Ripeti presentazione La presentazione è terminata Nessun media trovato per la presentazione + + Cambia modalità visualizzazione + Griglia + Elenco + Mostra cartelle nascoste Riproduci video automaticamente Visibilità nome del file - Mostra tipo di media - Solo immagini - Solo video - Solo GIF - Immagini, video, GIF - Immagini e video Ripeti i video Anima le GIF in miniatura Luminosità max durante visualizzazione @@ -113,7 +123,11 @@ Sfondo scuro a schermo intero Scorri miniature orizzontalmente Nascondi UI di sistema con media a schermo intero + Elimina cartelle vuote dopo averne eliminato il contenuto + Gestisci il volume e la luminosità dei video con gesti verticali Sostituisci Condividi con Ruota a schermo intero + Show extended details over fullscreen media + Manage extended details diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index a1f649c3a..93b6e616e 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -4,116 +4,130 @@ ギャラリー 編集 カメラを開く - …で開く + 別のアプリで開く 有効なアプリが見つかりません (非表示) - Pin folder - Unpin folder - Show all folders content - All media - Switch to folder view - Other folder - Show on map - Unknown location - No application with maps has been found - No Camera app has been found - Increase column count - Reduce column count - Change cover image - Select photo - Use default - Set as - Volume - Brightness + フォルダーをピン留めする + フォルダーのピン留めを外す + 全てを表示 + すべてのフォルダー + フォルダーを選択する + その他のフォルダー + 地図で表示 + 位置情報がありません + 地図アプリが見つかりません + カメラアプリが見つかりません + 列数を増やす + 列数を減らす + カバー画像を変更 + 写真を選択 + デフォルトに戻す + 他で使う + 音量 + 明るさ + Do not ask again in this session + + + 表示メディア種 + 画像 + ビデオ + GIF + 絞り込み条件に該当するメディアがありません。 + 絞り込み条件を変更 - This function hides the folder by adding a \'.nomedia\' file into it, it will hide all subfolders too. You can see them by toggling the \'Show hidden folders\' option in Settings. Continue? - Exclude - Excluded folders - Manage excluded folders - This will exclude the selection together with its subfolders from Simple Gallery only. You can manage excluded folders in Settings. - Exclude a parent instead? - Excluding folders will make them together with their subfolders hidden just in Simple Gallery, they will still be visible in other applications.\\n\\nIf you want to hide them from other apps too, use the Hide function. - Remove all - Remove all folders from the list of excluded? This will not delete the folders. + 対象のフォルダーに「.nomedia」というファイルを作成し、フォルダーを非表示にします。そのフォルダー以下のすべてのサブフォルダーも、同様に非表示となります。非表示となったフォルダーを見るには、「設定」の中にある「非表示のフォルダーを表示」オプションを切り替えてください。このフォルダーを非表示にしますか? + 除外する + 除外フォルダー + 除外フォルダーを管理 + 選択したフォルダーとそのサブフォルダーを、Simple Galleyの一覧から除外します。除外したフォルダーは「設定」で管理できます。 + 親フォルダーを選択して除外することもできます。 + フォルダーを除外すると、サブフォルダーも含めSimple Galleyの一覧から除外します。他のアプリでは引き続き表示されます。\\n\\n他のアプリでも非表示にしたい場合は、「非表示」機能を使用してください。 + すべて解除 + 除外するフォルダーの登録をすべて解除しますか? フォルダー自体は削除されません。 - Included folders - Manage included folders - Add folder - If you have some folders which contain media, but were not recognized by the app, you can add them manually here. + 追加フォルダー + 追加フォルダーを管理 + フォルダーを追加 + メディアを含んでいるフォルダーがアプリから認識されていない場合は、手動で追加できます。 - Resize - Resize selection and save - Width - Height - Keep aspect ratio - Please enter a valid resolution + リサイズ + 選択領域をリサイズして保存 + + 高さ + 縦横比を固定 + 解像度を正しく入力してください エディター 保存 回転 - Path + パス 無効な画像パス 画像の編集に失敗しました 画像を編集: 画像エディターが見つかりません ファイルの場所が不明です 元のファイルを上書きできません - Rotate left - Rotate right - Rotate by 180º - Flip - Flip horizontally - Flip vertically - Edit with + 左に回転 + 右に回転 + 180º回転 + 反転 + 水平方向に反転 + 垂直方向に反転 + 他のアプリで編集 シンプル壁紙 - 壁紙として設定 - 壁紙としての設定に失敗しました - 壁紙として設定: + 壁紙に設定 + 壁紙の設定に失敗しました + 壁紙に設定: 対応できるアプリが見つかりません - 壁紙の設定… + 壁紙に設定中… 壁紙を正常に設定しました - Portrait aspect ratio - Landscape aspect ratio + 縦向きの縦横比 + 横向きの縦横比 - Slideshow - Interval (seconds): - Include photos and GIFs - Include videos - Random order - Use fade animations - Move backwards - The slideshow ended - No media for the slideshow have been found + スライドショー + 間隔 (秒): + 写真を含める + ビデオを含める + GIFを含める + ランダムな順序 + フェードアニメーションを使用する + 逆方向に進む + スライドショーをリピート再生する + スライドショーが終了しました + スライドショーに表示するメディアがありません + + + Change view type + Grid + List 非表示フォルダーを表示 - 自動的にビデオを再生 + ビデオを自動再生する ファイル名の表示を切り替え - Show media - Images only - Videos only - GIFs only - Images, videos, GIFs - Images and videos - Loop videos - Animate GIFs at thumbnails - Max brightness when viewing media - Crop thumbnails into squares - Rotate fullscreen media by - System setting - Device rotation - Aspect ratio - Dark background at fullscreen media - Scroll thumbnails horizontally - Automatically hide system UI at fullscreen media - Replace Share with Rotate at fullscreen menu + ビデオをリピート再生する + GIF画像のサムネイルをアニメーション表示する + メディア再生時に明るさを最大にする + サムネイルを正方形に切り取る + メディア再生時のフルスクリーン表示切り替え + システム設定に従う + 端末の向きに従う + メディアの縦横比に従う + 黒背景でフルスクリーン表示 + サムネイル画面を横方向にスクロール + フルスクリーン時にシステムUIを非表示にする + メディアの削除後にフォルダーが空になった場合、そのフォルダーを削除する + ビデオ再生中に、音量と明るさを縦方向のジェスチャーで変更する + フルスクリーンメニューの「共有」を「回転」に置き換える + Show extended details over fullscreen media + Manage extended details diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 81cbaaae6..a148c3a0e 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -25,6 +25,15 @@ Ustaw jako Głośność Jasność + Do not ask again in this session + + + Filtruj multimedia + Obrazy + Filmy + GIFy + Nie znalazłem multimediów z wybranymi filtrami. + Zmień filtry Ta funkcja ukrywa foldery dodając do nich pusty plik .nomedia. Aby móc je zobaczyć, należy włączyć opcję \'Pokazuj ukryte foldery\' w ustawieniach. Kontyntynuować? @@ -49,7 +58,7 @@ Szerokość Wysokość Zachowaj proporcje - Wpisz poprawną rozdzielczość +    Podaj poprawną rozdzielczość Edycja @@ -84,24 +93,25 @@ Pokaz slajdów Przedział (sekundy): - Dołączaj zdjęcia i GIFy + Dołączaj zdjęcia Dołączaj filmy + Dołączaj GIFy Losowa kolejność Używaj płynnych przejść    Odwrotna kolejność +    Zapętlaj Pokaz slajdów zakończony Nie znalazłem multimediów do pokazu slajdów + +    Zmień typ widoku +    Siatka +    Lista + Pokazuj ukryte foldery Odtwarzaj filmy automatycznie Pokazuj / ukrywaj nazwy plików - Pokazywane typy multimediów - Tylko obrazy - Tylko filmy - Tylko GIFy - Obrazy, filmy i GIFy - Obrazy i filmy Zapętlaj odtwarzanie filmów Animowane miniatury GIFów Maksymalna jasność podczas wyświetlania multimediów @@ -113,13 +123,17 @@ Czarne tło przy podglądzie pełnoekranowym Przewijaj miniatury poziomo Ukrywaj interfejs przy pełnoekranowym podglądzie + Usuwaj puste foldery po usunięciu ich zawartości + Zezwalaj na kontrolę jasności i głośności filmów pionowymi gestami Zamień funkcję udostępniania na obracanie w menu pełnoekranowym + Show extended details over fullscreen media + Manage extended details Darmowa galeria bez reklam do przeglądania obrazów i filmów. - Prosta aplikacja galerii do oglądania obrazów i filmów. Pliki mogą być sortowane według daty, rozmiaru i nazwy, zarówno w porządku rosnącym jak i malejącym. W zależności od wielkości ekranu wyświetlane mogą być w wielu kolumnach. Liczbę kolumn można zmieniać za pomocą gestów. Zdjęcia mogą być powiększane, przycinane, obracane lub ustawiane jako tapeta bezpośrednio z poziomu aplikacji. Kolory aplikacji można dowolnie ustawiać. + Prosta aplikacja galerii do oglądania obrazów i filmów. Pliki mogą być sortowane według daty, rozmiaru i nazwy, zarówno w porządku rosnącym, jak i malejącym. W zależności od wielkości ekranu wyświetlane mogą być w wielu kolumnach. Liczbę kolumn można zmieniać za pomocą gestów. Zdjęcia mogą być powiększane, przycinane, obracane lub ustawiane jako tapeta bezpośrednio z poziomu aplikacji. Kolory aplikacji można dowolnie ustawiać. Aplikacja nie zawiera żadnych reklam ani niepotrzebnych uprawnień. Jest też w pełni otawrtoźrodłowa. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index ab1b9691c..30662f1d9 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -5,8 +5,7 @@ Editar Abrir câmera Abrir com - Nenhum aplicativo encontrado -     + Nenhum aplicativo encontrado     (oculto) Fixar pasta Desfixar pasta @@ -26,6 +25,15 @@ Set as Volume Brightness + Do not ask again in this session + + + Filter media + Images + Videos + GIFs + No media files have been found with the selected filters. + Change filters Esta opção oculta uma pasta com a adição de um arquivo \'.nomedia\' dentro dela, e irá ocultar todas as subpastas que estejam dentro da mesma. Você poderá rever essas pastas com a opção \'Mostrar pastas ocultas\'. Continuar? @@ -85,24 +93,25 @@ Slideshow Interval (seconds): - Include photos and GIFs + Include photos Include videos + Include GIFs Random order Use fade animations Move backwards + Loop slideshow The slideshow ended No media for the slideshow have been found + + Change view type + Grid + List + Mostrar pastas ocultas Reproduzir vídeos automaticamente Mostrar/ocultar nome do arquivo - Mostrar mídia - Apenas imagens - Apenas vídeos - Apenas GIFS - Imagens, vídeos, GIFs - Imagens e vídeos Reproduzir vídeos em ciclo Animação de GIFs nas miniaturas Brilho máximo ao visualizar mídia @@ -114,7 +123,11 @@ Fundo de tela escuro em mídia tela cheia Rolar miniaturas horizontalmente Automatically hide system UI at fullscreen media + Delete empty folders after deleting their content + Allow controlling video volume and brightness with vertical gestures Replace Share with Rotate at fullscreen menu + Show extended details over fullscreen media + Manage extended details diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 300f156b1..51ba03b5c 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -19,21 +19,30 @@ Não existe uma aplicação adequeada Aumentar número de colunas Diminuir número de colunas - Alterar imagem da capa + Alterar imagem de capa Selecionar foto Predefinição Definir como Volume Brilho + Do not ask again in this session + + + Filtrar multimédia + Imagens + Vídeos + GIFs + Não foram encontrados ficheiros que cumpram os requisitos. + Alterar filtros Esta opção oculta uma pasta com a adição de um ficheiro \'.nomedia\' na pasta, e irá ocultar todas as subpastas existentes. Pode ver as pastas com a opção \'Mostrar pastas ocultas\'. Continuar? Exclusão Pastas excluídas Gerir pastas excluídas - Esta ação apenas exclui as pastas selecionadas da lista de pastas desta aplicação. Pode gerir as pastas excuídas nas Definições. + Esta ação apenas exclui as pastas selecionadas da lista de pastas desta aplicação. Pode gerir as pastas excluídas nas Definições. Excluir antes a pasta superior? - A exlusão de uma pasta apenas oculta o seu conteúdo do Simple Gallery uma vez que as outras aplicações continuarão a poder aceder-lhes.\\n\\nSe quiser ocultar também das outras aplicações, utilize a função Ocultar. + A exclusão de uma pasta apenas oculta o seu conteúdo do Simple Gallery porque as outras aplicações continuarão a poder aceder-lhes.\\n\\nSe quiser ocultar também das outras aplicações, utilize a função Ocultar. Remover todas Remover todas as pastas de lista de exclusões? Esta ação não apaga as pastas. @@ -84,24 +93,25 @@ Apresentação Intervalo (segundos): - Incluir fotos e GIFs + Incluir fotos Incluir vídeos + Incluir GIFs Ordem aleatória Usar animações Mover para trás + Apresentação em ciclo Apresentação terminada Não foram encontrados ficheiros para a apresentação + + Tipo de exibição + Grelha + Lista + Mostrar pastas ocultas Reproduzir vídeos automaticamente Mostrar/ocultar nome do ficheiro - Mostrar multimédia - Apenas imagens - Apenas vídeos - Apenas GIFs - Imagens, vídeos e GIFs - Imagens e vídeos Vídeos em ciclo Animação de GIFs nas miniaturas Brilho máximo permitido @@ -113,7 +123,11 @@ Usar sempre um fundo escuro se em ecrã completo Deslocação horizontal de miniaturas Ocultar interface do sistema se em ecrã completo + Apagar as pastas vazias depois de remover o seu conteúdo + Permitir controlo do volume e brilho dos vídeos através de gestos verticais Substituir a opção Partilhar pela opção Rodar se em ecrã completo + Show extended details over fullscreen media + Manage extended details diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 700fc831e..34ffc082c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -19,29 +19,38 @@ Не найдено приложения камеры Добавить 1 столбец Убрать 1 столбец - Change cover image - Select photo - Use default - Set as - Volume - Brightness + Изменить обложку + Выбрать изображение + Использовать по умолчанию + Установить как… + Громкость + Яркость + Do not ask again in this session + + + Фильтр медиа + Изображения + Видео + GIF + При заданных фильтрах медиафайлы не найдены. + Изменить фильтры - Эта опция скрывает папку, добавляя в неё файл \'.nomedia\'; будут скрыты все подпапки. Можно показывать их, переключая \'Показать скрытые папки\' в настройках. Продолжить? + Эта функция скрывает папку, добавляя в неё файл \'.nomedia\'; будут скрыты все подпапки. Можно показывать их, переключая \'Показывать скрытые папки\' в настройках. Продолжить? Исключить Исключённые папки Управление исключёнными папками - Эта опция исключит выбранные папки вместе с подпапками только для Simple Gallery. Можно управлять исключёнными папками из настроек. + Эта функция исключит выбранные папки вместе с подпапками только для Simple Gallery. Можно управлять исключёнными папками из настроек. Исключить только родительский каталог? Исключая папки, вы сделаете их скрытыми вместе с подпапками в Simple Gallery, но они будут видны в других приложениях. Если вы хотите скрыть их в других приложениях, используйте функцию Скрыть. Удалить всё Очистить список исключённых? Сами папки не будут удалены. - Включенные папки - Управление включенными папками + Включённые папки + Управление включёнными папками Добавление папки - Если у вас есть папки содержащие медиа, но не распознанные Simple Gallery. Вы можете добавить их вручную. + Если у вас есть папки, содержащие медиафайлы, но не распознанные Simple Gallery. Вы можете добавить их вручную. Изменить размер @@ -64,11 +73,11 @@ Не удалось перезаписать исходный файл Повернуть влево Повернуть вправо - Перевернуть на 180º + Перевернуть на 180° Отразить По горизонтали По вертикали - Edit with + Редактировать в Простые обои @@ -76,44 +85,49 @@ Установить не удалось Установить в качестве обоев в: Приложение не найдено - Установка обоев… + Установка обоев… Обои успешно установлены Формат изображения Пейзажное соотношение сторон - Slideshow - Interval (seconds): - Include photos and GIFs - Include videos - Random order - Use fade animations - Move backwards - The slideshow ended - No media for the slideshow have been found + Слайдшоу + Интервал (секунды): + Включать изображения + Включать видео + Включать GIF + Случайный порядок + Эффект затухания + В обратном направлении + Закольцевать слайдшоу + Слайдшоу завершилось + Никаких медиафайлов для слайдшоу не было найдено. + + + Режим отображения + Сетка + Список - Показать скрытые папки + Показывать скрытые папки Воспроизводить видео автоматически Переключить отображение имени файла - Отображать - Только изображения - Только видео - GIFs only - Images, videos, GIFs - Изображения и видео Повторять видео - Анимировать эскизы GIF-файлов + Анимировать эскизы GIF Максимальная яркость при просмотре файлов Нарезать миниатюры в квадраты Полноэкранный поворот Системные настройки Поворот устройства Соотношение сторон - Dark background at fullscreen media - Scroll thumbnails horizontally - Automatically hide system UI at fullscreen media - Replace Share with Rotate at fullscreen menu + Тёмный фон в полноэкранном режиме + Прокрутка эскизов по горизонтали + Автоматически скрывать системный интерфейс в полноэкранном режиме + Удалять пустые папки после удаления их содержимого + Управлять громкостью и яркостью видео с помощью вертикальных жестов + Заменить \'Поделиться\' на \'Повернуть\' в меню полноэкранного режима + Show extended details over fullscreen media + Manage extended details diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index b9b0ea85c..50eeece79 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -25,6 +25,15 @@ Nastaviť ako Hlasitosť Jas + Nepýtať sa už v tomto spustení + + + Filter médií + Obrázky + Videá + GIFká + So zvolenými filtrami sa nenašli žiadne média súbory. + Zmeniť filtre Táto funkcia skryje priečinok pridaním súboru \'.nomedia\', skryté budú aj podpriečinky. Môžete ich vidieť zvolením možnosti \'Zobraziť skryté priečinky\' v nastaveniach. Pokračovať? @@ -41,7 +50,7 @@ Pridané priečinky Spravovať pridané priečinky Pridať priečinok - Ak máte nejaké priečinky obsahujúce médiá, ale neboli rozpoznané aplikáciou, môžete ich tu manuálne pridať. + Ak máte nejaké priečinky obsahujúce médiá, ale neboli rozpoznané aplikáciou, môžete ich tu manuálne pridať.\n\nPridanie nových položiek sem nevylúči žiadny iný priečinok. Zmeniť veľkosť @@ -84,24 +93,25 @@ Prezentácia Interval (sekundy): - Zahrnúť fotky a GIF súbory + Zahrnúť fotky Zahrnúť videá + Zahrnúť GIFy Náhodné poradie Používať miznúce animácie Ísť opačným smerom + Automaticky reštartovať prezentáciu Prezentácia skončila Pre prezentáciu sa nenašli žiadne vhodné súbory + + Zmeniť typ zobrazenia + Mriežka + Zoznam + Zobraziť skryté médiá Spúšťať videá automaticky Prepnúť viditeľnosť názvov súborov - Zobraziť médiá - Iba obrázky - Iba videá - Iba GIFká - Obrázky, videá, GIFká - Obrázky aj videá Automaticky reštartovať videá Animovať GIF súbory pri náhľade Maximálny jas pri prezeraní médií @@ -113,13 +123,17 @@ Tmavé pozadie pri médiách na celú obrazovku Prehliadať miniatúry vodorovne Automaticky skrývať systémové lišty pri celoobrazovkových médiách + Odstrániť prázdne priečinky po vymazaní ich obsahu + Povoliť ovládanie hlasitosti a jasu videí vertikálnymi ťahmi Nahradiť Zdieľanie s Otočením v celoobrazovkovom menu + Zobraziť rozšírené vlastnosti ponad celoobrazovkové médiá + Spravovať rozšírené vlastnosti Galéria na prezeranie obrázkov a videí bez reklám. - Jednoduchá nástroj použiteľný na prezeranie obrázkov a videí. Položky môžu byť zoradené podľa dátumu, veľkosti, názvu oboma smermi, obrázky je možné aj priblížiť. Položky sú zobrazované vo viacerých stĺpcoch v závislosti od veľkosti displeja, počet stĺpcov je možné meniť pomocou gesta prstami. Súbory môžete premenovať, zdieľať, mazať, kopírovať, premiestňovaŤ. Obrázky môžete orezať, otočiť, alebo nastaviť ako tapeta priamo v aplikácií. + Jednoduchá nástroj použiteľný na prezeranie obrázkov a videí. Položky môžu byť zoradené podľa dátumu, veľkosti, názvu oboma smermi, obrázky je možné aj priblížiť. Položky sú zobrazované vo viacerých stĺpcoch v závislosti od veľkosti displeja, počet stĺpcov je možné meniť pomocou gesta prstami. Súbory môžete premenovať, zdieľať, mazať, kopírovať, premiestňovať. Obrázky môžete orezať, otočiť, alebo nastaviť ako tapeta priamo v aplikácií. Galéria je tiež poskytovaná pre použitie treťou stranou pre prehliadanie fotiek a videí, pridávanie príloh v emailových klientoch. Je perfektná na každodenné použitie. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 68558ac8c..2ba4fc87d 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -23,8 +23,17 @@ Välj foto Använd standard Ange som - Volume - Brightness + Volym + Ljusstyrka + Do not ask again in this session + + + Filtrera media + Bilder + Videor + GIF-bilder + Inga mediefiler hittades med valda filter. + Ändra filter Denna funktion döljer mappen och alla dess undermappar genom att lägga till en \'.nomedia\'-fil i den. Du kan se dem genom att växla \'Visa dolda mappar\'-alternativet i Inställningar. Vill du fortsätta? @@ -41,7 +50,7 @@ Inkluderade mappar Hantera inkluderade mappar Lägg till mapp - Om du har vissa mappar som innehåller media men som inte känns igen av appen kan du lägga till dem manuellt här. + Om du har vissa mappar som innehåller media men som inte känns igen av appen, kan du lägga till dem manuellt här. Ändra storlek @@ -82,26 +91,27 @@ Liggande bildförhållande - Slideshow - Interval (seconds): - Include photos and GIFs - Include videos - Random order - Use fade animations - Move backwards - The slideshow ended - No media for the slideshow have been found + Bildspel + Intervall (sekunder): + Inkludera foton + Inkludera videor + Inkludera GIF-bilder + Spela upp i slumpmässig ordning + Använd toningsanimationer + Spela upp i omvänd ordning + Spela upp i en slinga + Bildspelet har avslutats + Ingen media hittades för bildspelet + + + Change view type + Grid + List Visa dolda mappar Spela upp videos automatiskt Visa/dölj filnamnen - Visa media - Endast bilder - Endast videos - Bara GIF-bilder - Bilder, videor, GIF-bilder - Bilder och videos Återspela videos Animera GIF-bilders miniatyrer Maximal ljusstyrka när media visas @@ -113,15 +123,19 @@ Mörk bakgrund när media visas i helskärmsläge Rulla horisontellt genom miniatyrer Dölj systemanvändargränssnittet automatiskt när media visas i helskärmsläge - Replace Share with Rotate at fullscreen menu + Ta bort tomma mappar när deras innehåll tas bort + Tillåt styrning av videovolym och videoljusstyrka med vertikala gester + Ersätt Dela med Rotera i helskärmsmenyn + Show extended details over fullscreen media + Manage extended details Ett Galleri för att visa bilder och videos utan en massa reklam. - Ett enkelt verktyg för att visa bilder och vdeos. Objekten kan sorteras efter datum, storlek, namn både stigande och fallande, bilder kan zoomas in. Mediafiler visas i flera kolumner beroende av skärmens storlek, du kan ändra antalet kolumner genom en nyp-rörelse. De går att döpa om, dela, ta bort, kopiera, flytta. Bilder kan också beskäras, roteras och anges som bakgrundsbild direkt från appen. + Ett enkelt verktyg för att visa bilder och videos. Objekten kan sorteras efter datum, storlek, namn både stigande och fallande, bilder kan zoomas in. Mediafiler visas i flera kolumner beroende av skärmens storlek, du kan ändra antalet kolumner genom en nyp-rörelse. De går att döpa om, dela, ta bort, kopiera, flytta. Bilder kan också beskäras, roteras och anges som bakgrundsbild direkt från appen. - Galleriet kan också användas av tredjeparts för förhandsgranskning av bilder / videos, bifoga bilagor i e-postklienter etc. Den är perfekt för det dagliga användandet. + Galleriet kan också användas av tredjepartsappar för förhandsgranskning av bilder / videos, bifoga bilagor i e-postklienter etc. Den är perfekt för det dagliga användandet. Innehåller ingen reklam eller onödiga behörigheter. Det är helt och hållet opensource, innehåller anpassningsbara färger. diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 087a7d896..6263ca620 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -25,6 +25,15 @@ Set as Volume Brightness + Do not ask again in this session + + + Filter media + Images + Videos + GIFs + No media files have been found with the selected filters. + Change filters Bu işlev, klasöre\'.medya yok\'dosyası ekleyerek gizler; tüm alt klasörleri de gizler. Bunları Ayarlar\'da\'Gizli klasörleri göster\'seçeneğine basarak görebilirsiniz. Devam et? @@ -84,24 +93,25 @@ Slideshow Interval (seconds): - Include photos and GIFs + Include photos Include videos + Include GIFs Random order Use fade animations Move backwards + Loop slideshow The slideshow ended No media for the slideshow have been found + + Change view type + Grid + List + Gizli klasörleri göster Videoları otomatik olarak oynat Dosya adı görünürlüğünü değiştir - Medyayı göster - Yalnızca resimler - Yalnızca videolar - GIFs only - Images, videos, GIFs - Resimler ve videolar Videolar döngüsü Küçük resimlerde GIF\'leri canlandırın Ortam görüntülerken azami parlaklık @@ -113,7 +123,11 @@ Dark background at fullscreen media Scroll thumbnails horizontally Automatically hide system UI at fullscreen media + Delete empty folders after deleting their content + Allow controlling video volume and brightness with vertical gestures Replace Share with Rotate at fullscreen menu + Show extended details over fullscreen media + Manage extended details diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 0dcda3787..2a6d8d78e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1,135 +1,341 @@ - Simple Gallery - 简约图库 - 编辑 - 打开相机 - 打开方式 - 未找到可用应用 - (隐藏) - 锁定目录 - 解除锁定目录 - 文件视图 + 转到主屏幕 + %1$s:%2$s + %1$s - %2$s:%3$s + 转到上一层级 + 更多选项 + 完成 + 查看全部 + 选择应用 + 关闭 + 开启 + 搜索… + 清除查询 + 搜索查询 + 搜索 + 提交查询 + 语音搜索 + 分享方式 + 通过%s分享 + 收起 + 指纹传感器有脏污。请擦拭干净,然后重试。 + 无法处理指纹,请重试。 + 仅检测到部分指纹,请重试。 + 手指移动太快,请重试。 + 手指移动太慢,请重试。 + 指纹操作已取消。 + 指纹硬件无法使用。 + 尝试次数过多,请稍后重试。 + 无法存储指纹。请移除一个现有的指纹。 + 指纹录入操作超时,请重试。 + 请重试。 + 无法识别 + 指纹: + 搜索 + 999+ + 关于 + 添加指纹 + 添加目录 + 额外信息 + 专辑 所有目录 - 目录视图 - 其他目录 - 在地图中显示 - 未知位置 - 未找到地图应用 - 未找到相机应用 - 增加一栏 - 减少一栏 - 更改封面图片 - 选择图片 - 使用默认 - 设置为 - 音量 + 使用纵向滑动手势控制视频音量和亮度 + 啊哦,出错啦: %s + GIF 缩略图 + 简约图库 + 一个观看照片和视频的简单实用工具。项目可以根据日期、大小、名称来递增或递减排序,照片可以缩放。媒体文件根据屏幕的大小排列在多个方格中,您可以使用缩放手势来调整每一列的方格数量。媒体文件可以被重命名、分享、删除、复制以及移动。照片亦可被剪切、旋转或是直接在应用中设为壁纸。 相册亦提供能让第三方应用预览图片/视频、向电子邮件客户端添加附件等的功能。非常适合日常使用。 应用不包含广告与不必要的权限。它是完全开放源代码的,并内置自定义颜色主题。 这个应用只是一系列应用中的一小部份。您可以在 http://www.simplemobiletools.com 找到其余的应用。 + 简约图库 + 一个没有广告,用来观看照片及视频的相册。 + 应用版本:%1$s + "应用到 '_1'" + 应用到全部冲突项 + 艺术家 + 递增 + 验证已被阻止,请稍后再试 + 验证失败 + 自动播放 + 背景色 亮度 + 相机 + 取消 + 更改封面图片 + 更改过滤器 + 更改视图类型 + 更改颜色将切换到自定义主题 + 点击此处以设置目标路径 + 确认选择 + 请选择 SD 卡根目录并授予写权限 + 如果您未找到 SD 卡目录,请尝试 + 确认外部存储器访问权限 + 复制 + 无法复制文件 + 复制/移动 + 操作失败 + 复制到 + 正在复制… + 复制成功 + 无法复制相同文件 + v %1$s Copyright © Simple Mobile Tools %2$d + 创建文件 %s 失败 + 创建文件夹 %s 失败 + 新建 + 新建文件夹 + 裁剪缩略图 + Android Image Cropper(图像裁剪和旋转) + 自定义 + 自定义颜色 + 全屏时黑色背景 + 深色主题 + 拍摄日期 + 删除 + 删除没有内容的空文件夹 + 递减 + 目标路径 + 设备系统:%1$s + 子目录数 + 丢弃 + 不再询问 + 捐赠 + "您已使用此应用一段时间了。 - - 通过添加文件 \'.nomedia\' 到目录,可以防止目录及其子目录下的所有媒体被扫描。您可以通过设置中的 \'显示隐藏目录\' 选项改变设置,是否继续? +可您知道么,我并不就职于一个大公司。开发应用耗时耗力,且我想依旧保持免费,您的捐赠会给我更多动力。 + +您可以通过购买付费应用 Simple Thank You 或捐赠比特币来支持我。 + +更多信息请查阅 http://simplemobiletools.com/donate. + +非常感谢!" + 捐赠 + 时长 + 编辑 + 编辑方式: + 编辑方式 + 编辑器 + 发送反馈 + 请输入名称 + 输入密码 + 不能覆盖源文件 排除 - 排除目录 - 管理排除目录 目录及其子目录中的媒体将不会在 Simple Gallery 中显示,您可以在设置更改。 是否排除父目录? 此目录及其子目录中的媒体将不会在 Simple Gallery 中显示,但是其它应用可以访问。如果您想对其它应用隐藏,请使用隐藏功能。 - 移除全部 - 是否移除列表中的全部目录?目录不会被物理删除。 - - - 包含目录 - 管理包含目录 - 添加目录 - 如果您还有应用未扫描到的媒体文件,请添加所在目录路径。 - - - 缩放 - 缩放选定区域并保存 - 宽度 - 高度 - 保持纵横比 - 请输入有效分辨率 - - - 编辑器 - 保存 - 旋转 - 路径 - 无效图片路径 - 图片编辑失败 - 编辑方式: - 未找到可用图片编辑器 - 未知的文件路径 - 不能覆盖源文件 - 向左旋转 - 向右旋转 - 旋转 180º + 排除目录 + EXIF + 曝光时间 + 扩展名 + 扩展名不能为空 + 超大 + 光圈 + 文件 + 文件 %1$s 已存在 + 文件 %1$s 已存在。是否覆盖? + 文件保存成功 + 文件名 + 文件名不能为空 + 文件名包含非法字符 + 总文件数 + 要显示的媒体文件 + 指纹 + 保护设置成功。请重新安装本应用,以防复位时出现问题。 翻转 水平翻转 垂直翻转 - 编辑方式 - - - Simple Wallpaper + 焦距 + 文件夹 + 目录视图 + 关注我们: + 字体大小 + GIFs + 滑动(图像加载和缓存) + 去设置 + + 你在寻找一个简单而匿名的视频通话应用吗?请尝试我的另一个项目 + 高度 + (隐藏) + 隐藏 + 隐藏文件夹 + "通过添加文件 '.nomedia' 到目录,可以防止目录及其子目录下的所有媒体被扫描。您可以通过设置中的 '显示隐藏目录' 选项改变设置,是否继续?" + 全屏时自动隐藏状态栏 + 图片编辑失败 + 图片 + 包含目录 + 包含 GIFs + 包含照片 + 包含视频 + 如果您还有应用未扫描到的媒体文件,请添加所在目录路径。 + 增加一栏 + 绘制图案 + 内部存储器 + 间隔(秒): + 无法写入到选中目标路径 + 无效文件格式 + 无效图片路径 + 名称包含非法字符 + 请输入有效分辨率 + 分享给好友 + 分享到 + ISO 速度 + 已选择项目 + Joda-Time(Java日期替换) + 保持纵横比 + Kotlin(编程语言) + 横向长宽比 + + 修改日期 + 浅色主题 + 列表 + 循环幻灯片 + 循环播放视频 + 管理排除目录 + 要显示的详细信息项目 + 管理包含目录 + 浏览时最大亮度 + + 更多应用 + 移动 + 倒播 + 移动到 + 正在移动… + 文件移动成功 + 无法移动相同文件 + RecyclerView MultiSelect(选择多个列表项) + 名称 + 同名文件夹或文件已存在 + + 未找到可用应用 + 未找到相机应用 + 未找到可用应用 + 未找到可用图片编辑器 + 未选择文件 + 您还没有注册指纹,请先给你的设备添加一些指纹 + 未找到地图应用 + 未发现可用媒体 + 所选的过滤器没有找到的媒体文件。 + 请授予权限以访问您的存储器 + 此应用使用了以下三方库。谢谢。 + 确认 + 打开相机 + 打开方式 + 其他目录 + Otto (event bus) + 内存不足 + 覆盖 + 使用密码保护隐藏项 + 路径 + 图案 + PatternLockView(图案保护) + PhotoView(可缩放 gifs) + Picasso(图像加载和缓存) + 密码 + 锁定目录 + 请将手指放在指纹传感器上 + 请输入密码 + 请选择目标路径 + 纵向长宽比 + 主体色 + 是否执行此删除操作? + 属性 + 密码设置成功。为防止遗忘,请重新安装本应用。 + 随机顺序 + 为我们打分 + 减少一栏 + 移除全部 + 是否移除列表中的全部目录?目录不会被物理删除。 + 重命名 + 重命名文件 + 无法重命名文件 + 重命名文件夹 + 文件夹名不能为空 + 无法重命名文件夹 + 文件夹名已存在 + 文件夹重命名成功 + 无法重命名存储器的根目录 + 重命名中... + 重复图案 + 重复密码 + 替换全屏时菜单栏的“分享”为“旋转” + 缩放 + 缩放选定区域并保存 + 分辨率 + 恢复默认 + 根目录 + 旋转 + 向左旋转 + 旋转 180º + 向右旋转 + RtlViewPager(从右到左滑动) + 保存 + 保存 + 您尚未保存更改,是否保存? + 正在保存… + 根据长宽比 + 全屏方向 + 设备方向 + 系统设置 + 水平滚动缩略图 + SD 卡 + 搜索 + 全选 + 选择操作 + 选择目标路径 + 选择文件 + 选择文件夹 + 选择图片 + 选择存储器 + 设置为 设为壁纸 壁纸设置失败 设为壁纸... - 未找到可用应用 - 正在设置壁纸… - 壁纸设置成功 - 纵向长宽比 - 横向长宽比 - - - 幻灯片 - 间隔(秒): - 包含照片和动态图 - 包含视频 - 随机顺序 - 使用渐变动画 - 倒播 - 幻灯片结束 - 未发现可用媒体 - - + 正在设置壁纸… + 设置 + 分享 + 请前往 %2$s 看看我们的 %1$s 吧! + 分享到 + 文件视图 + 全屏浏览媒体时显示详细信息 + 显示隐藏文件和文件夹 显示所有 - 自动播放 + 在地图中显示 + 简约壁纸 + 大小 + 跳过 + 幻灯片 + 幻灯片结束 + + 排序方式 + 原始路径 + 原始路径和目标路径不能相同 + Stetho(调试数据库) + 不显示隐藏的媒体文件 + Subsampling Scale Image View(可缩放图像浏览) + 显示/隐藏缓存内容 + 文本颜色 + 主题 + 开放源代码 + 开放源代码 + 标题 显示文件名 - 显示多媒体文件 - 仅图片 - 仅视频 - 仅 GIF - 图片,视频,GIF - 图片和视频 - 循环播放视频 - GIF 缩略图 - 浏览时最大亮度 - 裁剪缩略图 - 全屏时方向 - 系统设置 - 设备方向 - 根据长宽比 - 全屏时黑色背景 - 水平滚动缩略图 - 全屏时自动隐藏状态栏 - 替换全屏时菜单栏的“分享”为“旋转” - - - - 一个没有广告,用来观看照片及视频的相册。 - - 一个观看照片和视频的简单实用工具。项目可以根据日期、大小、名称来递增或递减排序,照片可以缩放。媒体文件根据屏幕的大小排列在多个方格中,您可以使用缩放手势来调整每一列的方格数量。媒体文件可以被重命名、分享、删除、复制以及移动。照片亦可被剪切、旋转或是直接在应用中设为壁纸。 - - 相册亦提供能让第三方应用预览图片/视频、向电子邮件客户端添加附件等的功能。非常适合日常使用。 - - 应用不包含广告与不必要的权限。它是完全开放源代码的,并内置自定义颜色主题。 - - 这个应用只是一系列应用中的一小部份。您可以在 http://www.simplemobiletools.com 找到其余的应用。 - - - + 撤销更改 + 是否撤销您的更改? + 取消隐藏 + 取消隐藏文件夹 + 未知错误 + 未知的文件路径 + 未知位置 + 解除锁定目录 + 使用默认 + 使用渐变动画 + 仅应用于此文件夹 + 视频 + 音量 + 壁纸设置成功 + 应用源码 + 更新日志 + * 此处仅列举了重大更新,更多修正可在使用中体验 + 宽度 + 图案错误 + 密码错误 + 目录选择错误,请选择 SD 卡 + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 61cc68907..1ec71fd20 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -25,6 +25,15 @@ 設為 音量 亮度 + Do not ask again in this session + + + 篩選媒體檔案 + 圖片 + 影片 + GIF + 選擇的篩選條件未發現媒體檔案。 + 更改篩選條件 這功能藉由添加一個\'.nomedia\'檔案,來隱藏資料夾和所有子資料夾。您可以透過設定中的「顯示隱藏的資料夾」選項來查看。\n是否繼續? @@ -84,24 +93,25 @@ 投影片 間隔 (秒): - 包含照片和GIF + Include photos 包含影片 + Include GIFs 隨機順序 使用淡入淡出動畫 反向播放 + Loop slideshow 投影片結束 找不到投影片的媒體檔案 + + Change view type + Grid + List + 顯示隱藏的媒體檔案 自動播放影片 顯示檔案名稱 - 顯示媒體檔案 - 只有圖片 - 只有影片 - 只有GIF - 圖片、影片、GIF - 圖片和影片 影片循環播放 縮圖顯示GIF動畫 瀏覽時最大亮度 @@ -113,7 +123,11 @@ 全螢幕時黑背景 橫向滑動縮圖 全螢幕時自動隱藏系統介面 + 刪除內容後刪除空白資料夾 + 允許用上下手勢來控制影片的音量和亮度 將全螢幕選單的分享取代為旋轉 + Show extended details over fullscreen media + Manage extended details diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index a67c01562..58e7e8c8e 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -3,9 +3,11 @@ 150dp 100dp 20dp + 22dp 26dp 150dp 24dp 50dp 150dp + 72dp diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 4f173fa1f..6aa0e6205 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -2,12 +2,22 @@ + Added an option to show customizable extended details over fullscreen media + + Added fingerprint to hidden item protection\n + Added a new List view type + + + Added a switch for disabling video gestures\n + Added a switch for deleting empty folders after deleting content + + Moved media type filter from Settings to the Action menu Allow changing the screen brightness and volume at videos by vertically dragging the screen sides Added slideshow at the fullscreen view Added pattern/pin protection for showing hidden items Added a toggle for replacing Share with Rotate at fullscreen media - Added an indicator of folders located on SD cards + Added an indicator of folders located on SD cards\n Improved the way of rotating jpg images on the internal storage by modifying the exif tags + added autosave Added an option for automatically hiding the system UI at entering fullscreen mode diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 496fb5445..fac61048c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,15 @@ Set as Volume Brightness + Do not ask again in this session + + + Filter media + Images + Videos + GIFs + No media files have been found with the selected filters. + Change filters This function hides the folder by adding a \'.nomedia\' file into it, it will hide all subfolders too. You can see them by toggling the \'Show hidden folders\' option in Settings. Continue? @@ -41,7 +50,7 @@ Included folders Manage included folders Add folder - If you have some folders which contain media, but were not recognized by the app, you can add them manually here. + If you have some folders which contain media, but were not recognized by the app, you can add them manually here.\n\nAdding some items here will not exclude any other folder. Resize @@ -84,24 +93,25 @@ Slideshow Interval (seconds): - Include photos and GIFs + Include photos Include videos + Include GIFs Random order Use fade animations Move backwards + Loop slideshow The slideshow ended No media for the slideshow have been found + + Change view type + Grid + List + Show hidden media Play videos automatically Toggle filename visibility - Show media - Images only - Videos only - GIFs only - Images, videos, GIFs - Images and videos Loop videos Animate GIFs at thumbnails Max brightness when viewing media @@ -113,7 +123,11 @@ Dark background at fullscreen media Scroll thumbnails horizontally Automatically hide system UI at fullscreen media + Delete empty folders after deleting their content + Allow controlling video volume and brightness with vertical gestures Replace Share with Rotate at fullscreen menu + Show extended details over fullscreen media + Manage extended details diff --git a/build.gradle b/build.gradle index 0a26ece58..633d20306 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,7 @@ allprojects { repositories { jcenter() maven { url "https://jitpack.io" } + maven { url "https://maven.google.com" } } }