diff --git a/CHANGELOG.md b/CHANGELOG.md index 32bca140f..58136d1a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ Changelog ========== +Version 6.1.1 *(2018-12-18)* +---------------------------- + + * Fixing some crashes + +Version 6.1.0 *(2018-12-17)* +---------------------------- + + * Added an initial widget implementation for creating homescreen folder shortcuts + * Added optional grouping of direct subfolders, as a check at the "Change view type" dialog + * Added an option to set custom crop aspect ratio at the editor + * Save exif data at edited files on Android 7+ + * Handle only Mass Storage USB devices, ignore the rest + * Many other smaller UX/stability/performance improvements + Version 6.0.4 *(2018-12-04)* ---------------------------- diff --git a/app/build.gradle b/app/build.gradle index aa7e98b3c..d3c104ce3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { applicationId "com.simplemobiletools.gallery.pro" minSdkVersion 21 targetSdkVersion 28 - versionCode 212 - versionName "6.0.4" + versionCode 214 + versionName "6.1.1" multiDexEnabled true setProperty("archivesBaseName", "gallery") } @@ -57,15 +57,15 @@ android { } dependencies { - implementation 'com.simplemobiletools:commons:5.5.4' + implementation 'com.simplemobiletools:commons:5.5.18' implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' implementation 'androidx.multidex:multidex:2.0.0' implementation 'it.sephiroth.android.exif:library:1.0.1' - implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.15' + implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.16' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2' implementation 'com.google.android.exoplayer:exoplayer-core:2.9.2' - implementation 'com.google.vr:sdk-panowidget:1.170.0' - implementation 'com.google.vr:sdk-videowidget:1.170.0' + implementation 'com.google.vr:sdk-panowidget:1.180.0' + implementation 'com.google.vr:sdk-videowidget:1.180.0' implementation 'org.apache.sanselan:sanselan:0.97-incubator' implementation 'info.androidhive:imagefilters:1.0.7' implementation 'com.squareup.picasso:picasso:2.71828' @@ -77,8 +77,9 @@ dependencies { annotationProcessor 'androidx.room:room-compiler:2.0.0' //implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0' - implementation 'com.github.tibbi:subsampling-scale-image-view:v3.10.1-fork' + //implementation 'com.github.tibbi:subsampling-scale-image-view:v3.10.1-fork' + implementation 'com.github.tibbi:subsampling-scale-image-view:fcb724fb0a' - // implementation 'com.github.chrisbanes:PhotoView:2.1.4' - implementation 'com.github.tibbi:PhotoView:2.2.1-fork' + // implementation 'com.github.chrisbanes:PhotoView:2.3.0' + implementation 'com.github.tibbi:PhotoView:2.3.0-fork' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a33b0c153..b00e56d71 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -215,6 +215,15 @@ + + + + + + + + + + + + + + ) { - // if hidden item showing is disabled but all Favorite items are hidden, hide the Favorites folder mIsGettingDirs = false mShouldStopFetching = false + // if hidden item showing is disabled but all Favorite items are hidden, hide the Favorites folder if (!config.shouldShowHidden) { val favoritesFolder = newDirs.firstOrNull { it.areFavorites() } if (favoritesFolder != null && favoritesFolder.tmb.getFilenameFromPath().startsWith('.')) { @@ -821,7 +823,7 @@ class MainActivity : SimpleActivity(), DirectoryOperationsListener { } // we are looping through the already displayed folders looking for changes, do not do anything if nothing changed - if (directory == newDir) { + if (directory.copy(subfoldersCount = 0, subfoldersMediaCount = 0) == newDir) { continue } @@ -835,7 +837,7 @@ class MainActivity : SimpleActivity(), DirectoryOperationsListener { types = newDir.types } - showSortedDirs(dirs) + setupAdapter(dirs) // update directories and media files in the local db, delete invalid items updateDBDirectory(directory, mDirectoryDao) @@ -862,7 +864,7 @@ class MainActivity : SimpleActivity(), DirectoryOperationsListener { mDirectoryDao.deleteDirPath(it.path) } dirs.removeAll(dirsToRemove) - showSortedDirs(dirs) + setupAdapter(dirs) } val foldersToScan = mediaFetcher.getFoldersToScan() @@ -899,7 +901,7 @@ class MainActivity : SimpleActivity(), DirectoryOperationsListener { val newDir = createDirectoryFromMedia(folder, newMedia, albumCovers, hiddenString, includedFolders, isSortingAscending) dirs.add(newDir) - showSortedDirs(dirs) + setupAdapter(dirs) mDirectoryDao.insert(newDir) if (folder != RECYCLE_BIN) { mMediumDao.insertAll(newMedia) @@ -936,18 +938,6 @@ class MainActivity : SimpleActivity(), DirectoryOperationsListener { directories_grid.beVisibleIf(directories_empty_text_label.isGone()) } - private fun showSortedDirs(dirs: ArrayList) { - val updatedDirs = getUniqueSortedDirs(dirs).toMutableList() as ArrayList - runOnUiThread { - (directories_grid.adapter as? DirectoryAdapter)?.updateDirs(updatedDirs) - } - } - - private fun getUniqueSortedDirs(dirs: ArrayList): ArrayList { - val distinctDirs = dirs.distinctBy { it.path.getDistinctPath() } as ArrayList - return getSortedDirectories(distinctDirs) - } - private fun createDirectoryFromMedia(path: String, curMedia: ArrayList, albumCovers: ArrayList, hiddenString: String, includedFolders: MutableSet, isSortingAscending: Boolean): Directory { var thumbnail = curMedia.firstOrNull { getDoesFilePathExist(it.path) }?.path ?: "" @@ -961,43 +951,59 @@ class MainActivity : SimpleActivity(), DirectoryOperationsListener { } } - val mediaTypes = curMedia.getDirMediaTypes() - val dirName = when (path) { - FAVORITES -> getString(R.string.favorites) - RECYCLE_BIN -> getString(R.string.recycle_bin) - else -> checkAppendingHidden(path, hiddenString, includedFolders) - } - val firstItem = curMedia.first() val lastItem = curMedia.last() + val dirName = checkAppendingHidden(path, hiddenString, includedFolders) val lastModified = if (isSortingAscending) Math.min(firstItem.modified, lastItem.modified) else Math.max(firstItem.modified, lastItem.modified) val dateTaken = if (isSortingAscending) Math.min(firstItem.taken, lastItem.taken) else Math.max(firstItem.taken, lastItem.taken) val size = curMedia.sumByLong { it.size } + val mediaTypes = curMedia.getDirMediaTypes() return Directory(null, path, thumbnail, dirName, curMedia.size, lastModified, dateTaken, size, getPathLocation(path), mediaTypes) } - private fun setupAdapter(dirs: ArrayList) { + private fun setupAdapter(dirs: ArrayList, textToSearch: String = "") { val currAdapter = directories_grid.adapter + val distinctDirs = dirs.distinctBy { it.path.getDistinctPath() }.toMutableList() as ArrayList + val sortedDirs = getSortedDirectories(distinctDirs) + var dirsToShow = getDirsToShow(sortedDirs, mDirs, mCurrentPathPrefix).clone() as ArrayList + if (currAdapter == null) { initZoomListener() val fastscroller = if (config.scrollHorizontally) directories_horizontal_fastscroller else directories_vertical_fastscroller - DirectoryAdapter(this, dirs.clone() as ArrayList, this, directories_grid, isPickIntent(intent) || isGetAnyContentIntent(intent), fastscroller) { - val path = (it as Directory).path - if (path != config.tempFolderPath) { - itemClicked(path) + DirectoryAdapter(this, dirsToShow, this, directories_grid, isPickIntent(intent) || isGetAnyContentIntent(intent), fastscroller) { + val clickedDir = it as Directory + val path = clickedDir.path + if (clickedDir.subfoldersCount == 1 || !config.groupDirectSubfolders) { + if (path != config.tempFolderPath) { + itemClicked(path) + } + } else { + mCurrentPathPrefix = path + mOpenedSubfolders.add(path) + setupAdapter(mDirs, "") } }.apply { setupZoomListener(mZoomListener) - directories_grid.adapter = this + runOnUiThread { + directories_grid.adapter = this + setupScrollDirection() + } } + measureRecyclerViewContent(dirsToShow) } else { - showSortedDirs(dirs) + if (textToSearch.isNotEmpty()) { + dirsToShow = dirsToShow.filter { it.name.contains(textToSearch, true) }.sortedBy { !it.name.startsWith(textToSearch, true) }.toMutableList() as ArrayList + } + runOnUiThread { + (directories_grid.adapter as? DirectoryAdapter)?.updateDirs(dirsToShow) + measureRecyclerViewContent(dirsToShow) + } } - getRecyclerAdapter()?.dirs?.apply { - measureRecyclerViewContent(this) - } - setupScrollDirection() + // recyclerview sometimes becomes empty at init/update, triggering an invisible refresh like this seems to work fine + directories_grid.postDelayed({ + directories_grid.scrollBy(0, 0) + }, 500) } private fun setupScrollDirection() { @@ -1051,7 +1057,7 @@ class MainActivity : SimpleActivity(), DirectoryOperationsListener { if (invalidDirs.isNotEmpty()) { dirs.removeAll(invalidDirs) - showSortedDirs(dirs) + setupAdapter(dirs) invalidDirs.forEach { mDirectoryDao.deleteDirPath(it.path) } @@ -1223,6 +1229,7 @@ class MainActivity : SimpleActivity(), DirectoryOperationsListener { add(Release(201, R.string.release_201)) add(Release(202, R.string.release_202)) add(Release(206, R.string.release_206)) + add(Release(213, R.string.release_213)) checkWhatsNew(this, BuildConfig.VERSION_CODE) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/activities/MediaActivity.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/activities/MediaActivity.kt index 791cabd78..536e05245 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/activities/MediaActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/activities/MediaActivity.kt @@ -118,6 +118,8 @@ class MediaActivity : SimpleActivity(), MediaOperationsListener { media_empty_text.setOnClickListener { showFilterMediaDialog() } + + updateWidgets() } override fun onStart() { @@ -339,7 +341,7 @@ class MediaActivity : SimpleActivity(), MediaOperationsListener { val dirName = when { mPath == FAVORITES -> getString(R.string.favorites) mPath == RECYCLE_BIN -> getString(R.string.recycle_bin) - mPath == OTG_PATH -> getString(R.string.otg) + mPath == OTG_PATH -> getString(R.string.usb) mPath.startsWith(OTG_PATH) -> mPath.trimEnd('/').substringAfterLast('/') else -> getHumanizedFilename(mPath) } @@ -585,7 +587,15 @@ class MediaActivity : SimpleActivity(), MediaOperationsListener { mCurrAsyncTask?.stopFetching() mCurrAsyncTask = GetMediaAsynctask(applicationContext, mPath, mIsGetImageIntent, mIsGetVideoIntent, mShowAll) { Thread { - gotMedia(it) + val oldMedia = mMedia.clone() as ArrayList + val newMedia = it + gotMedia(newMedia, false) + try { + oldMedia.filter { !newMedia.contains(it) }.mapNotNull { it as? Medium }.filter { !File(it.path).exists() }.forEach { + mMediumDao.deleteMediumPath(it.path) + } + } catch (e: Exception) { + } }.start() } @@ -689,8 +699,7 @@ class MediaActivity : SimpleActivity(), MediaOperationsListener { val pathToCheck = if (mPath.isEmpty()) SHOW_ALL else mPath val hasSections = config.getFolderGrouping(pathToCheck) and GROUP_BY_NONE == 0 && !config.scrollHorizontally val sectionTitleHeight = if (hasSections) layoutManager.getChildAt(0)?.height ?: 0 else 0 - val thumbnailHeight = if (hasSections) layoutManager.getChildAt(1)?.height ?: 0 else layoutManager.getChildAt(0)?.height - ?: 0 + val thumbnailHeight = if (hasSections) layoutManager.getChildAt(1)?.height ?: 0 else layoutManager.getChildAt(0)?.height ?: 0 var fullHeight = 0 var curSectionItems = 0 @@ -820,7 +829,7 @@ class MediaActivity : SimpleActivity(), MediaOperationsListener { } } - private fun gotMedia(media: ArrayList, isFromCache: Boolean = false) { + private fun gotMedia(media: ArrayList, isFromCache: Boolean) { mIsGettingMedia = false checkLastMediaChanged() mMedia = media diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/activities/ViewPagerActivity.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/activities/ViewPagerActivity.kt index 55dc39a86..9c96ec30e 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/activities/ViewPagerActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/activities/ViewPagerActivity.kt @@ -80,6 +80,7 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View var screenHeight = 0 } + @TargetApi(Build.VERSION_CODES.P) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_medium) @@ -337,6 +338,24 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View } } } + + if (intent.action == "com.android.camera.action.REVIEW") { + Thread { + if (galleryDB.MediumDao().getMediaFromPath(mPath).isEmpty()) { + val type = when { + mPath.isVideoFast() -> TYPE_VIDEOS + mPath.isGif() -> TYPE_GIFS + mPath.isSvg() -> TYPE_SVGS + mPath.isRawFast() -> TYPE_RAWS + else -> TYPE_IMAGES + } + + val duration = if (type == TYPE_VIDEOS) mPath.getVideoDuration() else 0 + val medium = Medium(null, mPath.getFilenameFromPath(), mPath, mPath.getParentPath(), System.currentTimeMillis(), System.currentTimeMillis(), File(mPath).length(), type, duration, false, 0) + galleryDB.MediumDao().insert(medium) + } + }.start() + } } private fun initBottomActions() { @@ -936,8 +955,16 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View } private fun askConfirmDelete() { - val message = if (config.useRecycleBin && !getCurrentMedium()!!.getIsInRecycleBin()) R.string.are_you_sure_recycle_bin else R.string.are_you_sure_delete - DeleteWithRememberDialog(this, getString(message)) { + val filename = "\"${getCurrentPath().getFilenameFromPath()}\"" + + val baseString = if (config.useRecycleBin && !getCurrentMedium()!!.getIsInRecycleBin()) { + R.string.move_to_recycle_bin_confirmation + } else { + R.string.deletion_confirmation + } + + val message = String.format(resources.getString(baseString), filename) + DeleteWithRememberDialog(this, message) { config.tempSkipDeleteConfirmation = it deleteConfirmed() } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/activities/WidgetConfigureActivity.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/activities/WidgetConfigureActivity.kt new file mode 100644 index 000000000..3a9050af2 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/activities/WidgetConfigureActivity.kt @@ -0,0 +1,184 @@ +package com.simplemobiletools.gallery.pro.activities + +import android.app.Activity +import android.appwidget.AppWidgetManager +import android.content.Intent +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.widget.RelativeLayout +import android.widget.RemoteViews +import com.simplemobiletools.commons.dialogs.ColorPickerDialog +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.gallery.pro.R +import com.simplemobiletools.gallery.pro.dialogs.PickDirectoryDialog +import com.simplemobiletools.gallery.pro.extensions.* +import com.simplemobiletools.gallery.pro.helpers.MyWidgetProvider +import com.simplemobiletools.gallery.pro.models.Directory +import com.simplemobiletools.gallery.pro.models.Widget +import kotlinx.android.synthetic.main.activity_widget_config.* + +class WidgetConfigureActivity : SimpleActivity() { + private var mBgAlpha = 0f + private var mWidgetId = 0 + private var mBgColor = 0 + private var mBgColorWithoutTransparency = 0 + private var mTextColor = 0 + private var mFolderPath = "" + private var mDirectories = ArrayList() + + public override fun onCreate(savedInstanceState: Bundle?) { + useDynamicTheme = false + super.onCreate(savedInstanceState) + setResult(RESULT_CANCELED) + setContentView(R.layout.activity_widget_config) + initVariables() + + mWidgetId = intent.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: AppWidgetManager.INVALID_APPWIDGET_ID + + if (mWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + finish() + } + + config_save.setOnClickListener { saveConfig() } + config_bg_color.setOnClickListener { pickBackgroundColor() } + config_text_color.setOnClickListener { pickTextColor() } + folder_picker_value.setOnClickListener { changeSelectedFolder() } + config_image_holder.setOnClickListener { changeSelectedFolder() } + folder_picker_show_folder_name.isChecked = config.showWidgetFolderName + handleFolderNameDisplay() + folder_picker_show_folder_name_holder.setOnClickListener { + folder_picker_show_folder_name.toggle() + handleFolderNameDisplay() + } + + updateTextColors(folder_picker_holder) + folder_picker_holder.background = ColorDrawable(config.backgroundColor) + + getCachedDirectories(false, false) { + mDirectories = it + val path = it.firstOrNull()?.path + if (path != null) { + updateFolderImage(path) + } + } + } + + private fun initVariables() { + mBgColor = config.widgetBgColor + if (mBgColor == 1) { + mBgColor = Color.BLACK + mBgAlpha = .2f + } else { + mBgAlpha = Color.alpha(mBgColor) / 255f + } + + mBgColorWithoutTransparency = Color.rgb(Color.red(mBgColor), Color.green(mBgColor), Color.blue(mBgColor)) + config_bg_seekbar.apply { + progress = (mBgAlpha * 100).toInt() + + onSeekBarChangeListener { + mBgAlpha = it / 100f + updateBackgroundColor() + } + } + updateBackgroundColor() + + mTextColor = config.widgetTextColor + updateTextColor() + } + + private fun saveConfig() { + val views = RemoteViews(packageName, R.layout.widget) + views.setBackgroundColor(R.id.widget_holder, mBgColor) + AppWidgetManager.getInstance(this).updateAppWidget(mWidgetId, views) + config.showWidgetFolderName = folder_picker_show_folder_name.isChecked + val widget = Widget(null, mWidgetId, mFolderPath) + Thread { + widgetsDB.insertOrUpdate(widget) + }.start() + + storeWidgetColors() + requestWidgetUpdate() + + Intent().apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId) + setResult(Activity.RESULT_OK, this) + } + finish() + } + + private fun storeWidgetColors() { + config.apply { + widgetBgColor = mBgColor + widgetTextColor = mTextColor + } + } + + private fun requestWidgetUpdate() { + Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, this, MyWidgetProvider::class.java).apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(mWidgetId)) + sendBroadcast(this) + } + } + + private fun updateBackgroundColor() { + mBgColor = mBgColorWithoutTransparency.adjustAlpha(mBgAlpha) + config_save.setBackgroundColor(mBgColor) + config_image_holder.setBackgroundColor(mBgColor) + config_bg_color.setFillWithStroke(mBgColor, Color.BLACK) + } + + private fun updateTextColor() { + config_save.setTextColor(mTextColor) + config_folder_name.setTextColor(mTextColor) + config_text_color.setFillWithStroke(mTextColor, Color.BLACK) + } + + private fun pickBackgroundColor() { + ColorPickerDialog(this, mBgColorWithoutTransparency) { wasPositivePressed, color -> + if (wasPositivePressed) { + mBgColorWithoutTransparency = color + updateBackgroundColor() + } + } + } + + private fun pickTextColor() { + ColorPickerDialog(this, mTextColor) { wasPositivePressed, color -> + if (wasPositivePressed) { + mTextColor = color + updateTextColor() + } + } + } + + private fun changeSelectedFolder() { + PickDirectoryDialog(this, "", false) { + updateFolderImage(it) + } + } + + private fun updateFolderImage(folderPath: String) { + mFolderPath = folderPath + runOnUiThread { + folder_picker_value.text = getFolderNameFromPath(folderPath) + config_folder_name.text = getFolderNameFromPath(folderPath) + } + + Thread { + val path = directoryDB.getDirectoryThumbnail(folderPath) + if (path != null) { + runOnUiThread { + loadJpg(path, config_image, config.cropThumbnails) + } + } + }.start() + } + + private fun handleFolderNameDisplay() { + val showFolderName = folder_picker_show_folder_name.isChecked + config_folder_name.beVisibleIf(showFolderName) + (config_image.layoutParams as RelativeLayout.LayoutParams).bottomMargin = if (showFolderName) 0 else resources.getDimension(R.dimen.normal_margin).toInt() + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/adapters/DirectoryAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/adapters/DirectoryAdapter.kt index b4241f18e..80ec519f3 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/adapters/DirectoryAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/adapters/DirectoryAdapter.kt @@ -38,6 +38,7 @@ class DirectoryAdapter(activity: BaseSimpleActivity, var dirs: ArrayList deleteFolders() else -> { val itemsCnt = selectedKeys.size - val items = resources.getQuantityString(R.plurals.delete_items, itemsCnt, itemsCnt) + val items = if (itemsCnt == 1) { + "\"${getSelectedPaths().first().getFilenameFromPath()}\"" + } else { + resources.getQuantityString(R.plurals.delete_items, itemsCnt, itemsCnt) + } + val fileDirItem = getFirstSelectedItem() ?: return val baseString = if (!config.useRecycleBin || (isOneItemSelected() && fileDirItem.isRecycleBin()) || (isOneItemSelected() && fileDirItem.areFavorites())) { R.string.deletion_confirmation @@ -501,9 +507,9 @@ class DirectoryAdapter(activity: BaseSimpleActivity, var dirs: ArrayList TYPE_VIDEOS directory.tmb.isGif() -> TYPE_GIFS diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/adapters/MediaAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/adapters/MediaAdapter.kt index 8731f0a30..e20ea1351 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/adapters/MediaAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/adapters/MediaAdapter.kt @@ -372,8 +372,15 @@ class MediaAdapter(activity: BaseSimpleActivity, var media: MutableList Unit) { + private var view: View + private var config = activity.config + + init { + view = activity.layoutInflater.inflate(R.layout.dialog_change_view_type, null).apply { + val viewToCheck = if (config.viewTypeFolders == VIEW_TYPE_GRID) change_view_type_dialog_radio_grid.id else change_view_type_dialog_radio_list.id + change_view_type_dialog_radio.check(viewToCheck) + change_view_type_dialog_group_direct_subfolders.isChecked = config.groupDirectSubfolders + } + + AlertDialog.Builder(activity) + .setPositiveButton(R.string.ok) { dialog, which -> dialogConfirmed() } + .setNegativeButton(R.string.cancel, null) + .create().apply { + activity.setupDialogStuff(view, this) + } + } + + private fun dialogConfirmed() { + val viewType = if (view.change_view_type_dialog_radio.checkedRadioButtonId == view.change_view_type_dialog_radio_grid.id) VIEW_TYPE_GRID else VIEW_TYPE_LIST + config.viewTypeFolders = viewType + config.groupDirectSubfolders = view.change_view_type_dialog_group_direct_subfolders.isChecked + callback() + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/CustomAspectRatioDialog.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/CustomAspectRatioDialog.kt new file mode 100644 index 000000000..830c839de --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/CustomAspectRatioDialog.kt @@ -0,0 +1,39 @@ +package com.simplemobiletools.gallery.pro.dialogs + +import android.widget.EditText +import androidx.appcompat.app.AlertDialog +import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.extensions.setupDialogStuff +import com.simplemobiletools.commons.extensions.showKeyboard +import com.simplemobiletools.commons.extensions.value +import com.simplemobiletools.gallery.pro.R +import kotlinx.android.synthetic.main.dialog_custom_aspect_ratio.view.* + +class CustomAspectRatioDialog(val activity: BaseSimpleActivity, val defaultCustomAspectRatio: Pair?, val callback: (aspectRatio: Pair) -> Unit) { + init { + val view = activity.layoutInflater.inflate(R.layout.dialog_custom_aspect_ratio, null).apply { + aspect_ratio_width.setText(defaultCustomAspectRatio?.first?.toString() ?: "") + aspect_ratio_height.setText(defaultCustomAspectRatio?.second?.toString() ?: "") + } + + AlertDialog.Builder(activity) + .setPositiveButton(R.string.ok, null) + .setNegativeButton(R.string.cancel, null) + .create().apply { + activity.setupDialogStuff(view, this) { + showKeyboard(view.aspect_ratio_width) + getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + val width = getViewValue(view.aspect_ratio_width) + val height = getViewValue(view.aspect_ratio_height) + callback(Pair(width, height)) + dismiss() + } + } + } + } + + private fun getViewValue(view: EditText): Int { + val textValue = view.value + return if (textValue.isEmpty()) 0 else textValue.toInt() + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/OtherAspectRatioDialog.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/OtherAspectRatioDialog.kt index 60cd259b6..4fe770c8c 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/OtherAspectRatioDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/OtherAspectRatioDialog.kt @@ -17,6 +17,7 @@ class OtherAspectRatioDialog(val activity: BaseSimpleActivity, val lastOtherAspe other_aspect_ratio_5_3.setOnClickListener { ratioPicked(Pair(5, 3)) } other_aspect_ratio_16_9.setOnClickListener { ratioPicked(Pair(16, 9)) } other_aspect_ratio_19_9.setOnClickListener { ratioPicked(Pair(19, 9)) } + other_aspect_ratio_custom.setOnClickListener { customRatioPicked() } other_aspect_ratio_1_2.setOnClickListener { ratioPicked(Pair(1, 2)) } other_aspect_ratio_2_3.setOnClickListener { ratioPicked(Pair(2, 3)) } @@ -46,6 +47,10 @@ class OtherAspectRatioDialog(val activity: BaseSimpleActivity, val lastOtherAspe else -> 0 } other_aspect_ratio_dialog_radio_2.check(radio2SelectedItemId) + + if (radio1SelectedItemId == 0 && radio2SelectedItemId == 0) { + other_aspect_ratio_dialog_radio_1.check(other_aspect_ratio_custom.id) + } } dialog = AlertDialog.Builder(activity) @@ -55,6 +60,13 @@ class OtherAspectRatioDialog(val activity: BaseSimpleActivity, val lastOtherAspe } } + private fun customRatioPicked() { + CustomAspectRatioDialog(activity, lastOtherAspectRatio) { + callback(it) + dialog.dismiss() + } + } + private fun ratioPicked(pair: Pair) { callback(pair) dialog.dismiss() diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/PickDirectoryDialog.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/PickDirectoryDialog.kt index 3bf2aa3d4..2adcb6835 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/PickDirectoryDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/PickDirectoryDialog.kt @@ -1,5 +1,6 @@ package com.simplemobiletools.gallery.pro.dialogs +import android.view.KeyEvent import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.RecyclerView import com.simplemobiletools.commons.activities.BaseSimpleActivity @@ -8,20 +9,20 @@ import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.views.MyGridLayoutManager import com.simplemobiletools.gallery.pro.R import com.simplemobiletools.gallery.pro.adapters.DirectoryAdapter -import com.simplemobiletools.gallery.pro.extensions.addTempFolderIfNeeded -import com.simplemobiletools.gallery.pro.extensions.config -import com.simplemobiletools.gallery.pro.extensions.getCachedDirectories -import com.simplemobiletools.gallery.pro.extensions.getSortedDirectories +import com.simplemobiletools.gallery.pro.extensions.* import com.simplemobiletools.gallery.pro.helpers.VIEW_TYPE_GRID import com.simplemobiletools.gallery.pro.models.Directory import kotlinx.android.synthetic.main.dialog_directory_picker.view.* -class PickDirectoryDialog(val activity: BaseSimpleActivity, val sourcePath: String, val callback: (path: String) -> Unit) { - var dialog: AlertDialog - var shownDirectories = ArrayList() - var view = activity.layoutInflater.inflate(R.layout.dialog_directory_picker, null) - var isGridViewType = activity.config.viewTypeFolders == VIEW_TYPE_GRID - var showHidden = activity.config.shouldShowHidden +class PickDirectoryDialog(val activity: BaseSimpleActivity, val sourcePath: String, showOtherFolderButton: Boolean, val callback: (path: String) -> Unit) { + private var dialog: AlertDialog + private var shownDirectories = ArrayList() + private var allDirectories = ArrayList() + private var openedSubfolders = arrayListOf("") + private var view = activity.layoutInflater.inflate(R.layout.dialog_directory_picker, null) + private var isGridViewType = activity.config.viewTypeFolders == VIEW_TYPE_GRID + private var showHidden = activity.config.shouldShowHidden + private var currentPathPrefix = "" init { (view.directories_grid.layoutManager as MyGridLayoutManager).apply { @@ -29,22 +30,32 @@ class PickDirectoryDialog(val activity: BaseSimpleActivity, val sourcePath: Stri spanCount = if (isGridViewType) activity.config.dirColumnCnt else 1 } - dialog = AlertDialog.Builder(activity) + val builder = 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_destination) { - view.directories_show_hidden.beVisibleIf(!context.config.shouldShowHidden) - view.directories_show_hidden.setOnClickListener { - activity.handleHiddenFolderPasswordProtection { - view.directories_show_hidden.beGone() - showHidden = true - fetchDirectories(true) - } - } + .setOnKeyListener { dialogInterface, i, keyEvent -> + if (keyEvent.action == KeyEvent.ACTION_UP && i == KeyEvent.KEYCODE_BACK) { + backPressed() + } + true + } + + if (showOtherFolderButton) { + builder.setNeutralButton(R.string.other_folder) { dialogInterface, i -> showOtherFolder() } + } + + dialog = builder.create().apply { + activity.setupDialogStuff(view, this, R.string.select_destination) { + view.directories_show_hidden.beVisibleIf(!context.config.shouldShowHidden) + view.directories_show_hidden.setOnClickListener { + activity.handleHiddenFolderPasswordProtection { + view.directories_show_hidden.beGone() + showHidden = true + fetchDirectories(true) } } + } + } fetchDirectories(false) } @@ -52,6 +63,10 @@ class PickDirectoryDialog(val activity: BaseSimpleActivity, val sourcePath: Stri private fun fetchDirectories(forceShowHidden: Boolean) { activity.getCachedDirectories(forceShowHidden = forceShowHidden) { if (it.isNotEmpty()) { + it.forEach { + it.subfoldersMediaCount = it.mediaCnt + } + activity.runOnUiThread { gotDirectories(activity.addTempFolderIfNeeded(it)) } @@ -66,18 +81,32 @@ class PickDirectoryDialog(val activity: BaseSimpleActivity, val sourcePath: Stri } private fun gotDirectories(newDirs: ArrayList) { - val dirs = activity.getSortedDirectories(newDirs) - if (dirs.hashCode() == shownDirectories.hashCode()) + if (allDirectories.isEmpty()) { + allDirectories = newDirs.clone() as ArrayList + } + val distinctDirs = newDirs.distinctBy { it.path.getDistinctPath() }.toMutableList() as ArrayList + val sortedDirs = activity.getSortedDirectories(distinctDirs) + val dirs = activity.getDirsToShow(sortedDirs, allDirectories, currentPathPrefix).clone() as ArrayList + if (dirs.hashCode() == shownDirectories.hashCode()) { return + } shownDirectories = dirs val adapter = DirectoryAdapter(activity, dirs.clone() as ArrayList, null, view.directories_grid, true) { - if ((it as Directory).path.trimEnd('/') == sourcePath) { - activity.toast(R.string.source_and_destination_same) - return@DirectoryAdapter + val clickedDir = it as Directory + val path = clickedDir.path + if (clickedDir.subfoldersCount == 1 || !activity.config.groupDirectSubfolders) { + if (path.trimEnd('/') == sourcePath) { + activity.toast(R.string.source_and_destination_same) + return@DirectoryAdapter + } else { + callback(path) + dialog.dismiss() + } } else { - callback(it.path) - dialog.dismiss() + currentPathPrefix = path + openedSubfolders.add(path) + gotDirectories(allDirectories) } } @@ -105,4 +134,18 @@ class PickDirectoryDialog(val activity: BaseSimpleActivity, val sourcePath: Stri } } } + + private fun backPressed() { + if (activity.config.groupDirectSubfolders) { + if (currentPathPrefix.isEmpty()) { + dialog.dismiss() + } else { + openedSubfolders.removeAt(openedSubfolders.size - 1) + currentPathPrefix = openedSubfolders.last() + gotDirectories(allDirectories) + } + } else { + dialog.dismiss() + } + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/PickMediumDialog.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/PickMediumDialog.kt index ae5675356..b1f83527b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/PickMediumDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/PickMediumDialog.kt @@ -53,7 +53,7 @@ class PickMediumDialog(val activity: BaseSimpleActivity, val path: String, val c } private fun showOtherFolder() { - PickDirectoryDialog(activity, path) { + PickDirectoryDialog(activity, path, true) { callback(it) dialog.dismiss() } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/ResizeDialog.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/ResizeDialog.kt index c7c10050e..96ef3b50b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/ResizeDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/ResizeDialog.kt @@ -6,11 +6,11 @@ import androidx.appcompat.app.AlertDialog import com.simplemobiletools.commons.activities.BaseSimpleActivity import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.gallery.pro.R -import kotlinx.android.synthetic.main.resize_image.view.* +import kotlinx.android.synthetic.main.dialog_resize_image.view.* class ResizeDialog(val activity: BaseSimpleActivity, val size: Point, val callback: (newSize: Point) -> Unit) { init { - val view = activity.layoutInflater.inflate(R.layout.resize_image, null) + val view = activity.layoutInflater.inflate(R.layout.dialog_resize_image, null) val widthView = view.image_width val heightView = view.image_height @@ -69,7 +69,7 @@ class ResizeDialog(val activity: BaseSimpleActivity, val size: Point, val callba } } - fun getViewValue(view: EditText): Int { + private fun getViewValue(view: EditText): Int { val textValue = view.value return if (textValue.isEmpty()) 0 else textValue.toInt() } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/extensions/Activity.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/extensions/Activity.kt index 21cf823ce..0c8458d92 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/extensions/Activity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/extensions/Activity.kt @@ -149,6 +149,7 @@ fun BaseSimpleActivity.removeNoMedia(path: String, callback: (() -> Unit)? = nul } tryDeleteFileDirItem(file.toFileDirItem(applicationContext), false, false) { + scanPathRecursively(file.parent) callback?.invoke() } } @@ -183,7 +184,7 @@ fun BaseSimpleActivity.tryCopyMoveFilesTo(fileDirItems: ArrayList, } val source = fileDirItems[0].getParentPath() - PickDirectoryDialog(this, source) { + PickDirectoryDialog(this, source, true) { copyMoveFilesTo(fileDirItems, source.trimEnd('/'), it, isCopyOperation, true, config.shouldShowHidden, callback) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/extensions/Context.kt index d7430f161..c8a7f5b58 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/extensions/Context.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/extensions/Context.kt @@ -1,5 +1,7 @@ package com.simplemobiletools.gallery.pro.extensions +import android.appwidget.AppWidgetManager +import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.res.Configuration @@ -25,6 +27,7 @@ import com.simplemobiletools.gallery.pro.databases.GalleryDatabase import com.simplemobiletools.gallery.pro.helpers.* import com.simplemobiletools.gallery.pro.interfaces.DirectoryDao import com.simplemobiletools.gallery.pro.interfaces.MediumDao +import com.simplemobiletools.gallery.pro.interfaces.WidgetsDao import com.simplemobiletools.gallery.pro.models.Directory import com.simplemobiletools.gallery.pro.models.Medium import com.simplemobiletools.gallery.pro.models.ThumbnailItem @@ -32,6 +35,10 @@ import com.simplemobiletools.gallery.pro.svg.SvgSoftwareLayerSetter import com.simplemobiletools.gallery.pro.views.MySquareImageView import pl.droidsonroids.gif.GifDrawable import java.io.File +import java.util.HashSet +import java.util.LinkedHashSet +import kotlin.Comparator +import kotlin.collections.ArrayList val Context.portrait get() = resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT val Context.audioManager get() = getSystemService(Context.AUDIO_SERVICE) as AudioManager @@ -104,6 +111,10 @@ val Context.config: Config get() = Config.newInstance(applicationContext) val Context.galleryDB: GalleryDatabase get() = GalleryDatabase.getInstance(applicationContext) +val Context.widgetsDB: WidgetsDao get() = GalleryDatabase.getInstance(applicationContext).WidgetsDao() + +val Context.directoryDB: DirectoryDao get() = GalleryDatabase.getInstance(applicationContext).DirectoryDao() + val Context.recycleBin: File get() = filesDir val Context.recycleBinPath: String get() = filesDir.absolutePath @@ -159,6 +170,10 @@ fun Context.getSortedDirectories(source: ArrayList): ArrayList o1.taken.compareTo(o2.taken) } + if (result == 0) { + result = AlphanumericComparator().compare(o1.path.toLowerCase(), o2.path.toLowerCase()) + } + if (sorting and SORT_DESCENDING != 0) { result *= -1 } @@ -168,6 +183,120 @@ fun Context.getSortedDirectories(source: ArrayList): ArrayList, allDirs: ArrayList, currentPathPrefix: String): ArrayList { + return if (config.groupDirectSubfolders) { + dirs.forEach { + it.subfoldersCount = 0 + it.subfoldersMediaCount = it.mediaCnt + } + + val dirFolders = dirs.map { it.path }.sorted().toMutableSet() as HashSet + val foldersToShow = getDirectParentSubfolders(dirFolders, currentPathPrefix) + val parentDirs = dirs.filter { foldersToShow.contains(it.path) } as ArrayList + updateSubfolderCounts(dirs, parentDirs) + + // show the current folder as an available option too, not just subfolders + if (currentPathPrefix.isNotEmpty()) { + val currentFolder = allDirs.firstOrNull { parentDirs.firstOrNull { it.path == currentPathPrefix } == null && it.path == currentPathPrefix } + currentFolder?.apply { + subfoldersCount = 1 + parentDirs.add(this) + } + } + + parentDirs + } else { + dirs.forEach { it.subfoldersMediaCount = it.mediaCnt } + dirs + } +} + +fun Context.getDirectParentSubfolders(folders: HashSet, currentPathPrefix: String): HashSet { + val internalPath = internalStoragePath + val sdPath = sdCardPath + val currentPaths = LinkedHashSet() + + folders.forEach { + val path = it + if (path != RECYCLE_BIN && path != FAVORITES && !path.equals(internalPath, true) && !path.equals(sdPath, true)) { + if (currentPathPrefix.isNotEmpty()) { + if (path == currentPathPrefix || File(path).parent.equals(currentPathPrefix, true)) { + currentPaths.add(path) + } + } else if (folders.any { !it.equals(path, true) && (File(path).parent.equals(it, true) || File(it).parent.equals(File(path).parent, true)) }) { + // if we have folders like + // /storage/emulated/0/Pictures/Images and + // /storage/emulated/0/Pictures/Screenshots, + // but /storage/emulated/0/Pictures is empty, show Images and Screenshots as separate folders, do not group them at /Pictures + val parent = File(path).parent + if (folders.contains(parent)) { + currentPaths.add(parent) + } else { + currentPaths.add(path) + } + } else { + currentPaths.add(path) + } + } + } + + var areDirectSubfoldersAvailable = false + currentPaths.forEach { + val path = it + currentPaths.forEach { + if (!it.equals(path) && File(it).parent?.equals(path) == true) { + areDirectSubfoldersAvailable = true + } + } + } + + if (currentPathPrefix.isEmpty() && folders.contains(RECYCLE_BIN)) { + currentPaths.add(RECYCLE_BIN) + } + + if (currentPathPrefix.isEmpty() && folders.contains(FAVORITES)) { + currentPaths.add(FAVORITES) + } + + if (folders.size == currentPaths.size) { + return currentPaths + } + + folders.clear() + folders.addAll(currentPaths) + return if (areDirectSubfoldersAvailable) { + getDirectParentSubfolders(folders, currentPathPrefix) + } else { + folders + } +} + +fun Context.updateSubfolderCounts(children: ArrayList, parentDirs: ArrayList) { + for (child in children) { + var longestSharedPath = "" + for (parentDir in parentDirs) { + if (parentDir.path == child.path) { + longestSharedPath = child.path + continue + } + + if (child.path.startsWith(parentDir.path, true) && parentDir.path.length > longestSharedPath.length) { + longestSharedPath = parentDir.path + } + } + + // make sure we count only the proper direct subfolders, grouped the same way as on the main screen + parentDirs.firstOrNull { it.path == longestSharedPath }?.apply { + if (path.equals(child.path, true) || path.equals(File(child.path).parent, true) || children.any { it.path.equals(File(child.path).parent, true) }) { + subfoldersCount++ + if (path != child.path) { + subfoldersMediaCount += child.mediaCnt + } + } + } + } +} + fun Context.getNoMediaFolders(callback: (folders: ArrayList) -> Unit) { Thread { val folders = ArrayList() @@ -234,10 +363,21 @@ fun Context.storeDirectoryItems(items: ArrayList, directoryDao: Direc } fun Context.checkAppendingHidden(path: String, hidden: String, includedFolders: MutableSet): String { - val dirName = when (path) { + val dirName = getFolderNameFromPath(path) + return if (File(path).doesThisOrParentHaveNoMedia() && !path.isThisOrParentIncluded(includedFolders)) { + "$dirName $hidden" + } else { + dirName + } +} + +fun Context.getFolderNameFromPath(path: String): String { + return when (path) { internalStoragePath -> getString(R.string.internal) sdCardPath -> getString(R.string.sd_card) - OTG_PATH -> getString(R.string.otg) + OTG_PATH -> getString(R.string.usb) + FAVORITES -> getString(R.string.favorites) + RECYCLE_BIN -> getString(R.string.recycle_bin) else -> { if (path.startsWith(OTG_PATH)) { path.trimEnd('/').substringAfterLast('/') @@ -246,12 +386,6 @@ fun Context.checkAppendingHidden(path: String, hidden: String, includedFolders: } } } - - return if (File(path).doesThisOrParentHaveNoMedia() && !path.isThisOrParentIncluded(includedFolders)) { - "$dirName $hidden" - } else { - dirName - } } fun Context.loadImage(type: Int, path: String, target: MySquareImageView, horizontalScroll: Boolean, animateGifs: Boolean, cropThumbnails: Boolean) { @@ -482,3 +616,14 @@ fun Context.getUpdatedDeletedMedia(mediumDao: MediumDao): ArrayList { fun Context.deleteDBPath(mediumDao: MediumDao, path: String) { mediumDao.deleteMediumPath(path.replaceFirst(recycleBinPath, RECYCLE_BIN)) } + +fun Context.updateWidgets() { + val widgetIDs = AppWidgetManager.getInstance(applicationContext).getAppWidgetIds(ComponentName(applicationContext, MyWidgetProvider::class.java)) + if (widgetIDs.isNotEmpty()) { + Intent(applicationContext, MyWidgetProvider::class.java).apply { + action = AppWidgetManager.ACTION_APPWIDGET_UPDATE + putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetIDs) + sendBroadcast(this) + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/fragments/PhotoFragment.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/fragments/PhotoFragment.kt index 0580e2a48..372bc7b0c 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/fragments/PhotoFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/fragments/PhotoFragment.kt @@ -26,6 +26,9 @@ import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestOptions import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView +import com.davemorrissey.labs.subscaleview.decoder.DecoderFactory +import com.davemorrissey.labs.subscaleview.decoder.ImageDecoder +import com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.OTG_PATH import com.simplemobiletools.gallery.pro.R @@ -377,18 +380,26 @@ class PhotoFragment : ViewPagerFragment() { val path = getPathToLoad(medium) isSubsamplingVisible = true + val bitmapDecoder = object : DecoderFactory { + override fun make() = PicassoDecoder(path, Picasso.get(), rotation) + } + + val regionDecoder = object : DecoderFactory { + override fun make() = PicassoRegionDecoder() + } + view.subsampling_view.apply { setMaxTileSize(if (context!!.config.showHighestQuality) Integer.MAX_VALUE else 4096) setMinimumTileDpi(if (context!!.config.showHighestQuality) -1 else getMinTileDpi()) background = ColorDrawable(Color.TRANSPARENT) - setBitmapDecoderFactory { PicassoDecoder(path, Picasso.get(), rotation) } - setRegionDecoderFactory { PicassoRegionDecoder() } + setBitmapDecoderFactory(bitmapDecoder) + setRegionDecoderFactory(regionDecoder) maxScale = 10f beVisible() isQuickScaleEnabled = context.config.oneFingerZoom setResetScaleOnSizeChange(false) setImage(ImageSource.uri(path)) - orientation = rotation + setOrientation(rotation) setEagerLoadingEnabled(false) setOnImageEventListener(object : SubsamplingScaleImageView.OnImageEventListener { override fun onImageLoaded() { @@ -402,7 +413,7 @@ class PhotoFragment : ViewPagerFragment() { mOriginalSubsamplingScale = scale } - override fun onTileLoadError(e: Exception?) { + override fun onTileLoadError(e: Exception) { } override fun onPreviewReleased() { @@ -415,7 +426,7 @@ class PhotoFragment : ViewPagerFragment() { beGone() } - override fun onPreviewLoadError(e: Exception?) { + override fun onPreviewLoadError(e: Exception) { background = ColorDrawable(Color.TRANSPARENT) isSubsamplingVisible = false beGone() diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/helpers/Config.kt index e25a4073d..cdb7ef840 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/helpers/Config.kt @@ -423,4 +423,12 @@ class Config(context: Context) : BaseConfig(context) { var lastEditorCropOtherAspectRatioY: Int get() = prefs.getInt(LAST_EDITOR_CROP_OTHER_ASPECT_RATIO_Y, 1) set(lastEditorCropOtherAspectRatioY) = prefs.edit().putInt(LAST_EDITOR_CROP_OTHER_ASPECT_RATIO_Y, lastEditorCropOtherAspectRatioY).apply() + + var groupDirectSubfolders: Boolean + get() = prefs.getBoolean(GROUP_DIRECT_SUBFOLDERS, false) + set(groupDirectSubfolders) = prefs.edit().putBoolean(GROUP_DIRECT_SUBFOLDERS, groupDirectSubfolders).apply() + + var showWidgetFolderName: Boolean + get() = prefs.getBoolean(SHOW_WIDGET_FOLDER_NAME, true) + set(showWidgetFolderName) = prefs.edit().putBoolean(SHOW_WIDGET_FOLDER_NAME, showWidgetFolderName).apply() } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/helpers/Constants.kt index f868a1747..95a068eb0 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/helpers/Constants.kt @@ -71,6 +71,8 @@ const val ALLOW_DOWN_GESTURE = "allow_down_gesture" const val LAST_EDITOR_CROP_ASPECT_RATIO = "last_editor_crop_aspect_ratio" const val LAST_EDITOR_CROP_OTHER_ASPECT_RATIO_X = "last_editor_crop_other_aspect_ratio_x" const val LAST_EDITOR_CROP_OTHER_ASPECT_RATIO_Y = "last_editor_crop_other_aspect_ratio_y" +const val GROUP_DIRECT_SUBFOLDERS = "group_direct_subfolders" +const val SHOW_WIDGET_FOLDER_NAME = "show_widget_folder_name" // slideshow const val SLIDESHOW_INTERVAL = "slideshow_interval" diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/helpers/MediaFetcher.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/helpers/MediaFetcher.kt index 24d571dfa..b57e37c70 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/helpers/MediaFetcher.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/helpers/MediaFetcher.kt @@ -364,6 +364,10 @@ class MediaFetcher(val context: Context) { else -> o1.taken.compareTo(o2.taken) } + if (result == 0) { + result = AlphanumericComparator().compare(o1.path.toLowerCase(), o2.path.toLowerCase()) + } + if (sorting and SORT_DESCENDING != 0) { result *= -1 } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/helpers/MyWidgetProvider.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/helpers/MyWidgetProvider.kt new file mode 100644 index 000000000..2af024791 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/helpers/MyWidgetProvider.kt @@ -0,0 +1,85 @@ +package com.simplemobiletools.gallery.pro.helpers + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.widget.RemoteViews +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.request.RequestOptions +import com.simplemobiletools.commons.extensions.setBackgroundColor +import com.simplemobiletools.commons.extensions.setText +import com.simplemobiletools.commons.extensions.setVisibleIf +import com.simplemobiletools.gallery.pro.R +import com.simplemobiletools.gallery.pro.activities.MediaActivity +import com.simplemobiletools.gallery.pro.extensions.* +import com.simplemobiletools.gallery.pro.models.Widget + +class MyWidgetProvider : AppWidgetProvider() { + private fun setupAppOpenIntent(context: Context, views: RemoteViews, id: Int, widget: Widget) { + val intent = Intent(context, MediaActivity::class.java).apply { + putExtra(DIRECTORY, widget.folderPath) + } + + val pendingIntent = PendingIntent.getActivity(context, widget.widgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT) + views.setOnClickPendingIntent(id, pendingIntent) + } + + override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { + super.onUpdate(context, appWidgetManager, appWidgetIds) + Thread { + val config = context.config + context.widgetsDB.getWidgets().filter { appWidgetIds.contains(it.widgetId) }.forEach { + val views = RemoteViews(context.packageName, R.layout.widget).apply { + setBackgroundColor(R.id.widget_holder, config.widgetBgColor) + setVisibleIf(R.id.widget_folder_name, config.showWidgetFolderName) + setTextColor(R.id.widget_folder_name, config.widgetTextColor) + setText(R.id.widget_folder_name, context.getFolderNameFromPath(it.folderPath)) + } + + val path = context.directoryDB.getDirectoryThumbnail(it.folderPath) ?: return@forEach + val options = RequestOptions() + .signature(path.getFileSignature()) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + if (context.config.cropThumbnails) options.centerCrop() else options.fitCenter() + + val density = context.resources.displayMetrics.density + val appWidgetOptions = appWidgetManager.getAppWidgetOptions(appWidgetIds.first()) + val width = appWidgetOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) + val height = appWidgetOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) + + val widgetSize = (Math.max(width, height) * density).toInt() + try { + val image = Glide.with(context) + .asBitmap() + .load(path) + .apply(options) + .submit(widgetSize, widgetSize) + .get() + views.setImageViewBitmap(R.id.widget_imageview, image) + } catch (e: Exception) { + } + + setupAppOpenIntent(context, views, R.id.widget_holder, it) + appWidgetManager.updateAppWidget(it.widgetId, views) + } + }.start() + } + + override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle) { + super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) + onUpdate(context, appWidgetManager, intArrayOf(appWidgetId)) + } + + override fun onDeleted(context: Context, appWidgetIds: IntArray) { + super.onDeleted(context, appWidgetIds) + Thread { + appWidgetIds.forEach { + context.widgetsDB.deleteWidgetId(it) + } + }.start() + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/interfaces/DirectoryDao.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/interfaces/DirectoryDao.kt index 78641ebf9..57d6807c2 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/interfaces/DirectoryDao.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/interfaces/DirectoryDao.kt @@ -29,4 +29,7 @@ interface DirectoryDao { @Query("DELETE FROM directories WHERE path = \'$RECYCLE_BIN\' COLLATE NOCASE") fun deleteRecycleBin() + + @Query("SELECT thumbnail FROM directories WHERE path = :path") + fun getDirectoryThumbnail(path: String): String? } diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/interfaces/WidgetsDao.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/interfaces/WidgetsDao.kt new file mode 100644 index 000000000..b6ecb2492 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/interfaces/WidgetsDao.kt @@ -0,0 +1,19 @@ +package com.simplemobiletools.gallery.pro.interfaces + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.simplemobiletools.gallery.pro.models.Widget + +@Dao +interface WidgetsDao { + @Query("SELECT * FROM widgets") + fun getWidgets(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrUpdate(widget: Widget): Long + + @Query("DELETE FROM widgets WHERE widget_id = :widgetId") + fun deleteWidgetId(widgetId: Int) +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/models/Directory.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/models/Directory.kt index c5b90b860..fd320050e 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/models/Directory.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/models/Directory.kt @@ -1,9 +1,6 @@ package com.simplemobiletools.gallery.pro.models -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.Index -import androidx.room.PrimaryKey +import androidx.room.* import com.simplemobiletools.commons.extensions.formatDate import com.simplemobiletools.commons.extensions.formatSize import com.simplemobiletools.commons.helpers.SORT_BY_DATE_MODIFIED @@ -12,7 +9,6 @@ import com.simplemobiletools.commons.helpers.SORT_BY_PATH import com.simplemobiletools.commons.helpers.SORT_BY_SIZE import com.simplemobiletools.gallery.pro.helpers.FAVORITES import com.simplemobiletools.gallery.pro.helpers.RECYCLE_BIN -import java.io.Serializable @Entity(tableName = "directories", indices = [Index(value = ["path"], unique = true)]) data class Directory( @@ -24,12 +20,14 @@ data class Directory( @ColumnInfo(name = "last_modified") var modified: Long, @ColumnInfo(name = "date_taken") var taken: Long, @ColumnInfo(name = "size") var size: Long, - @ColumnInfo(name = "location") val location: Int, - @ColumnInfo(name = "media_types") var types: Int) : Serializable { + @ColumnInfo(name = "location") var location: Int, + @ColumnInfo(name = "media_types") var types: Int, - companion object { - private const val serialVersionUID = -6553345863555455L - } + // used with "Group direct subfolders" enabled + @Ignore var subfoldersCount: Int = 0, + @Ignore var subfoldersMediaCount: Int = 0) { + + constructor() : this(null, "", "", "", 0, 0L, 0L, 0L, 0, 0, 0, 0) fun getBubbleText(sorting: Int) = when { sorting and SORT_BY_NAME != 0 -> name diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/models/Widget.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/models/Widget.kt new file mode 100644 index 000000000..2ef525334 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/models/Widget.kt @@ -0,0 +1,12 @@ +package com.simplemobiletools.gallery.pro.models + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity(tableName = "widgets", indices = [(Index(value = ["widget_id"], unique = true))]) +data class Widget( + @PrimaryKey(autoGenerate = true) var id: Int?, + @ColumnInfo(name = "widget_id") var widgetId: Int, + @ColumnInfo(name = "folder_path") var folderPath: String) diff --git a/app/src/main/res/drawable-hdpi/img_widget_preview.png b/app/src/main/res/drawable-hdpi/img_widget_preview.png new file mode 100644 index 000000000..625e39531 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/img_widget_preview.png differ diff --git a/app/src/main/res/drawable-xhdpi/img_widget_preview.png b/app/src/main/res/drawable-xhdpi/img_widget_preview.png new file mode 100644 index 000000000..481681a63 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/img_widget_preview.png differ diff --git a/app/src/main/res/drawable-xxhdpi/img_widget_preview.png b/app/src/main/res/drawable-xxhdpi/img_widget_preview.png new file mode 100644 index 000000000..c400e88e9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/img_widget_preview.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/img_widget_preview.png b/app/src/main/res/drawable-xxxhdpi/img_widget_preview.png new file mode 100644 index 000000000..ffe566b2d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/img_widget_preview.png differ diff --git a/app/src/main/res/layout/activity_widget_config.xml b/app/src/main/res/layout/activity_widget_config.xml new file mode 100644 index 000000000..ae762ab01 --- /dev/null +++ b/app/src/main/res/layout/activity_widget_config.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +