Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE REQUEST] Open and create .url shortcut files #4420

Merged
merged 20 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fa90f50
feat: added new string and created ShortcutDialogFragment
Aitorbp Jun 10, 2024
936bb55
feat: added icon to resources and in item list and implemented in FAB
Aitorbp Jun 10, 2024
69c1f3e
feat: added release notes
Aitorbp Jun 10, 2024
9e594f4
feat: prevent to open url files in PreviewTextFragment
Aitorbp Jun 10, 2024
a2b010c
feat: create, upload and open url functionality
Aitorbp Jun 10, 2024
d5a7577
refactor: changed naming and refactor
Aitorbp Jun 11, 2024
171b1b0
fix: removed doc in newInstance, button yes listener moved to enableY…
Aitorbp Jun 13, 2024
1534a65
refactor: changed name variable, remove activity from method, improve…
Aitorbp Jun 13, 2024
1bf073b
fix: release notes changes
Aitorbp Jun 13, 2024
f7adef7
chore: add calens
Aitorbp Jun 13, 2024
4f3f59b
docs: calens changelog updated
Aitorbp Jun 13, 2024
efaa56a
refactor: using MAX_FILENAME_LENGTH and forbiddenChars from MainFileL…
Aitorbp Jun 13, 2024
2944056
fix: improve ope Shortcut file dialog, fix algorithm enableYesButton,…
Aitorbp Jun 13, 2024
c382fb8
fix: changed named Yes to Create, change position name and url, and a…
Aitorbp Jun 17, 2024
38e66d3
fix: default https format url and 223 file name length fixed
Aitorbp Jun 17, 2024
3607101
fix: added url icon to upload list
Aitorbp Jun 17, 2024
a479bc9
fix: added scrollview in open shortcut and added extensión url to name
Aitorbp Jun 18, 2024
f90153d
fix: changed "spaces" to "blanks" in create_shortcut_dialog_url_error…
Aitorbp Jun 18, 2024
e6b9064
fix: changed formatUrl func from CreateShortcutDialogFragment to open…
Aitorbp Jun 18, 2024
d36ccf3
fix: Added again formatUrl func to CreateShortcutDialogFragment
Aitorbp Jun 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ ownCloud admins and users.
* Enhancement - Improvements in remove dialog alert: [#4342](https://github.com/owncloud/android/issues/4342)
* Enhancement - Content description in UI elements to improve accessibility: [#4360](https://github.com/owncloud/android/issues/4360)
* Enhancement - Added contentDescription attribute in the previewed image: [#4360](https://github.com/owncloud/android/issues/4360)
* Enhancement - Support for URL shortcut files: [#4413](https://github.com/owncloud/android/issues/4413)

## Details

Expand Down Expand Up @@ -277,6 +278,14 @@ ownCloud admins and users.
https://github.com/owncloud/android/issues/4360
https://github.com/owncloud/android/pull/4388

* Enhancement - Support for URL shortcut files: [#4413](https://github.com/owncloud/android/issues/4413)

A new option has been added in the FAB to create a shortcut file with a .url
extension. When the file is clicked, the URL will open in the browser.

https://github.com/owncloud/android/issues/4413
https://github.com/owncloud/android/pull/4420

# Changelog for ownCloud Android Client [4.2.2] (2024-05-30)

The following sections list the changes in ownCloud Android Client 4.2.2 relevant to
Expand Down
6 changes: 6 additions & 0 deletions changelog/unreleased/4420
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Support for URL shortcut files

A new option has been added in the FAB to create a shortcut file with a .url extension. When the file is clicked, the URL will open in the browser.

https://github.com/owncloud/android/issues/4413
https://github.com/owncloud/android/pull/4420
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* ownCloud Android client application
*
* @author Aitor Ballesteros Pavón
*
* Copyright (C) 2024 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.owncloud.android.presentation.files.createshortcut

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.DialogFragment
import com.owncloud.android.R
import com.owncloud.android.databinding.CreateShortcutDialogBinding
import com.owncloud.android.domain.files.model.OCFile
import com.owncloud.android.presentation.files.filelist.MainFileListFragment.Companion.MAX_FILENAME_LENGTH
import com.owncloud.android.presentation.files.filelist.MainFileListFragment.Companion.forbiddenChars
import com.owncloud.android.ui.activity.FileDisplayActivity

class CreateShortcutDialogFragment : DialogFragment() {
private lateinit var parentFolder: OCFile
private lateinit var createShortcutListener: CreateShortcutListener
private var _binding: CreateShortcutDialogBinding? = null
private val binding get() = _binding!!
JuancaG05 marked this conversation as resolved.
Show resolved Hide resolved
private var isCreateShortcutButtonEnabled = false

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = CreateShortcutDialogBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
handleInputsUrlAndFileName()
cancelButton.setOnClickListener {
dialog?.dismiss()
}
}
}

private fun handleInputsUrlAndFileName() {
var isValidUrl = false
var isValidFileName = false
var hasForbiddenCharacters: Boolean
var hasMaxCharacters: Boolean
var hasEmptyValue: Boolean
binding.createShortcutDialogNameFileValue.doOnTextChanged { fileNameValue, _, _, _ ->
fileNameValue?.let {
hasForbiddenCharacters = forbiddenChars.any { fileNameValue.contains(it) }
hasMaxCharacters = fileNameValue.length > MAX_FILENAME_LENGTH
isValidFileName = fileNameValue.isNotBlank() && !hasForbiddenCharacters && !hasMaxCharacters
handleNameRequirements(hasForbiddenCharacters, hasMaxCharacters)
updateCreateShortcutButtonState(isValidFileName, isValidUrl)
}
}
binding.createShortcutDialogUrlValue.doOnTextChanged { urlValue, _, _, _ ->
urlValue?.let {
hasEmptyValue = urlValue.contains(" ")
isValidUrl = urlValue.isNotBlank() && !hasEmptyValue
handleUrlRequirements(hasEmptyValue)
updateCreateShortcutButtonState(isValidFileName, isValidUrl)
}
}
}

private fun updateCreateShortcutButtonState(isValidFileName: Boolean, isValidUrl: Boolean) {
isCreateShortcutButtonEnabled = isValidFileName && isValidUrl
enableCreateButton(isCreateShortcutButtonEnabled)
}

private fun handleNameRequirements(hasForbiddenCharacters: Boolean, hasMaxCharacters: Boolean) {
binding.createShortcutDialogNameFileLayout.apply {
error = when {
hasMaxCharacters -> getString(R.string.uploader_upload_text_dialog_filename_error_length_max, MAX_FILENAME_LENGTH)
hasForbiddenCharacters -> getString(R.string.filename_forbidden_characters)
else -> null
}
}
}

private fun handleUrlRequirements(hasSpace: Boolean) {
binding.createShortcutDialogUrlLayout.apply {
if (hasSpace) {
error = getString(R.string.create_shortcut_dialog_url_error_no_blanks)
} else {
error = null
}
}
}

private fun enableCreateButton(enable: Boolean) {
binding.createButton.apply {
isEnabled = enable
if (enable) {
setOnClickListener {
createShortcutListener.createShortcutFileFromApp(
fileName = binding.createShortcutDialogNameFileValue.text.toString(),
url = formatUrl(binding.createShortcutDialogUrlValue.text.toString()),
)
dialog?.dismiss()
}
setTextColor(resources.getColor(R.color.primary_button_background_color, null))
} else {
setOnClickListener(null)
setTextColor(resources.getColor(R.color.grey, null))
}
}
}

private fun formatUrl(url: String): String {
var formattedUrl = url
if (!url.startsWith(FileDisplayActivity.PROTOCOL_HTTP) && !url.startsWith(FileDisplayActivity.PROTOCOL_HTTPS)) {
formattedUrl = FileDisplayActivity.PROTOCOL_HTTPS + url
}
return formattedUrl
}

interface CreateShortcutListener {
fun createShortcutFileFromApp(fileName: String, url: String)
}

companion object {

fun newInstance(parentFolder: OCFile, listener: CreateShortcutListener): CreateShortcutDialogFragment {
return CreateShortcutDialogFragment().apply {
createShortcutListener = listener
this.parentFolder = parentFolder
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import com.owncloud.android.domain.files.model.FileListOption
import com.owncloud.android.domain.files.model.OCFileWithSyncInfo
import com.owncloud.android.domain.files.model.OCFooterFile
import com.owncloud.android.presentation.authentication.AccountUtils
import com.owncloud.android.ui.activity.FileDisplayActivity.Companion.MIMETYPE_TEXT_URI_LIST
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.MimetypeIconUtil
import com.owncloud.android.utils.PreferenceUtils
Expand Down Expand Up @@ -304,6 +305,8 @@ class FileListAdapter(
if (file.isFolder) {
// Folder
fileIcon.setImageResource(R.drawable.ic_menu_archive)
} else if (file.mimeType == MIMETYPE_TEXT_URI_LIST) {
fileIcon.setImageResource(R.drawable.ic_action_open_shortcut)
} else {
// Set file icon depending on its mimetype. Ask for thumbnail later.
fileIcon.setImageResource(MimetypeIconUtil.getFileTypeIconId(file.mimeType, file.fileName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ import com.owncloud.android.presentation.files.SortOrder
import com.owncloud.android.presentation.files.SortType
import com.owncloud.android.presentation.files.ViewType
import com.owncloud.android.presentation.files.createfolder.CreateFolderDialogFragment
import com.owncloud.android.presentation.files.createshortcut.CreateShortcutDialogFragment
import com.owncloud.android.presentation.files.operations.FileOperation
import com.owncloud.android.presentation.files.operations.FileOperationsViewModel
import com.owncloud.android.presentation.files.removefile.RemoveFilesDialogFragment
Expand All @@ -126,14 +127,16 @@ import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import timber.log.Timber
import java.io.File

class MainFileListFragment : Fragment(),
CreateFolderDialogFragment.CreateFolderListener,
FileListAdapter.FileListAdapterListener,
SearchView.OnQueryTextListener,
SortDialogListener,
SortOptionsView.CreateFolderListener,
SortOptionsView.SortOptionsListener {
SortOptionsView.SortOptionsListener,
CreateShortcutDialogFragment.CreateShortcutListener {

private val mainFileListViewModel by viewModel<MainFileListViewModel> {
parametersOf(
Expand Down Expand Up @@ -824,6 +827,7 @@ class MainFileListFragment : Fragment(),
}
registerFabUploadListener()
registerFabMkDirListener()
registerFabNewShortcutListener()
}
}

Expand Down Expand Up @@ -872,6 +876,17 @@ class MainFileListFragment : Fragment(),
}
}

/**
* Registers [android.view.View.OnClickListener] on the 'New shortcut' mini FAB for the linked action.
*/
private fun registerFabNewShortcutListener() {
binding.fabNewshortcut.setOnClickListener {
val dialog = CreateShortcutDialogFragment.newInstance(mainFileListViewModel.getFile(), this)
dialog.show(requireActivity().supportFragmentManager, DIALOG_CREATE_SHORTCUT)
collapseFab()
}
}

fun collapseFab() {
binding.fabMain.collapse()
}
Expand Down Expand Up @@ -1003,6 +1018,18 @@ class MainFileListFragment : Fragment(),
updateActionModeAfterTogglingSelected()
}

override fun createShortcutFileFromApp(fileName: String, url: String) {
val fileContent = """
[InternetShortcut]
URL=$url
""".trimIndent()
val storageDir = requireActivity().externalCacheDir
val shortcutFile = File(storageDir, "$fileName.url")
shortcutFile.writeText(fileContent)
val shortcutFilePath = shortcutFile.absolutePath
uploadActions?.uploadShortcutFileFromApp(arrayOf(shortcutFilePath))
}

override fun onFolderNameSet(newFolderName: String, parentFolder: OCFile) {
fileOperationsViewModel.performOperation(FileOperation.CreateFolder(newFolderName, parentFolder))
fileOperationsViewModel.createFolder.observe(viewLifecycleOwner, Event.EventObserver { uiResult: UIResult<Unit> ->
Expand Down Expand Up @@ -1477,17 +1504,19 @@ class MainFileListFragment : Fragment(),

interface UploadActions {
fun uploadFromCamera()
fun uploadShortcutFileFromApp(shortcutFilePath: Array<String>)
fun uploadFromFileSystem()
}

companion object {
val ARG_PICKING_A_FOLDER = "${MainFileListFragment::class.java.canonicalName}.ARG_PICKING_A_FOLDER}"
val ARG_INITIAL_FOLDER_TO_DISPLAY = "${MainFileListFragment::class.java.canonicalName}.ARG_INITIAL_FOLDER_TO_DISPLAY}"
val ARG_FILE_LIST_OPTION = "${MainFileListFragment::class.java.canonicalName}.FILE_LIST_OPTION}"
private const val MAX_FILENAME_LENGTH = 223
private val forbiddenChars = listOf('/', '\\')
const val MAX_FILENAME_LENGTH = 223
val forbiddenChars = listOf('/', '\\')

private const val DIALOG_CREATE_FOLDER = "DIALOG_CREATE_FOLDER"
private const val DIALOG_CREATE_SHORTCUT = "DIALOG_CREATE_SHORTCUT"

private const val TAG_SECOND_FRAGMENT = "SECOND_FRAGMENT"
private const val FILE_DOCXF_EXTENSION = "docxf"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ class ReleaseNotesViewModel(
subtitle = R.string.release_notes_4_3_0_subtitle_av_offline_files_removed_locally_with_local_only_option,
type = ReleaseNoteType.BUGFIX
),
ReleaseNote(
title = R.string.release_notes_4_3_0_title_create_open_shortcut,
subtitle = R.string.release_notes_4_3_0_subtitle_create_open_shortcut,
type = ReleaseNoteType.ENHANCEMENT
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
* ownCloud Android client application
*
* @author Juan Carlos Garrote Gascón
* @author Aitor Ballesteros Pavón
*
* Copyright (C) 2023 ownCloud GmbH.
* Copyright (C) 2024 ownCloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
Expand Down Expand Up @@ -129,16 +130,18 @@ class TransfersAdapter(

uploadStatus.isVisible = transferItem.transfer.status != TransferStatus.TRANSFER_SUCCEEDED
uploadStatus.text = " — " + holder.itemView.context.getString(transferItem.transfer.statusToStringRes())

val placeholderIcon = if (transferItem.transfer.localPath.endsWith(".url")) {
R.drawable.ic_action_open_shortcut
} else {
MimetypeIconUtil.getFileTypeIconId(
MimetypeIconUtil.getBestMimeTypeByFilename(transferItem.transfer.localPath),
fileName
)
}
Glide.with(holder.itemView)
.load(transferItem.transfer.localPath)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(
MimetypeIconUtil.getFileTypeIconId(
MimetypeIconUtil.getBestMimeTypeByFilename(transferItem.transfer.localPath),
fileName
)
)
.placeholder(placeholderIcon)
.into(thumbnail)

uploadRightButton.isVisible = transferItem.transfer.status != TransferStatus.TRANSFER_SUCCEEDED
Expand Down
Loading
Loading