Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions changelog/unreleased/4952
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Enhancement: Create a share over a file or a folder on an oCIS server

A new option to create a share over a file or a folder on an oCIS has been added.
It will be only visible for users with proper permissions.

https://github.com/owncloud/android/issues/4875
https://github.com/owncloud/android/issues/4917
https://github.com/owncloud/android/pull/4952
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import com.owncloud.android.domain.members.usecases.SearchMembersUseCase
import com.owncloud.android.domain.roles.usecases.GetRolesAsyncUseCase
import com.owncloud.android.domain.server.usecases.GetServerInfoAsyncUseCase
import com.owncloud.android.domain.sharing.sharees.GetShareesAsyncUseCase
import com.owncloud.android.domain.sharing.shares.usecases.AddGraphShareAsyncUseCase
import com.owncloud.android.domain.sharing.shares.usecases.CreatePrivateShareAsyncUseCase
import com.owncloud.android.domain.sharing.shares.usecases.CreatePublicShareAsyncUseCase
import com.owncloud.android.domain.sharing.shares.usecases.DeleteShareAsyncUseCase
Expand Down Expand Up @@ -229,6 +230,7 @@ val useCaseModule = module {
factoryOf(::UnsetFilesAsAvailableOfflineUseCase)

// Sharing
factoryOf(::AddGraphShareAsyncUseCase)
factoryOf(::CreatePrivateShareAsyncUseCase)
factoryOf(::CreatePublicShareAsyncUseCase)
factoryOf(::DeleteShareAsyncUseCase)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* ownCloud Android client application
*
* @author Jorge Aguado Recio
*
* Copyright (C) 2026 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.extensions

import androidx.core.view.isVisible
import com.owncloud.android.R
import com.owncloud.android.databinding.AddMemberFragmentBinding
import com.owncloud.android.domain.members.model.OCMember
import com.owncloud.android.domain.members.model.OCMemberType
import com.owncloud.android.presentation.spaces.members.SpaceRolesAdapter

fun AddMemberFragmentBinding.showOrHideEmptyView(hasMembers: Boolean, searchMinLength: Int) {
membersRecyclerView.isVisible = hasMembers
emptyDataParent.apply {
val shouldShow = !hasMembers && searchBar.query.length >= searchMinLength
root.isVisible = shouldShow
if (shouldShow) {
listEmptyDatasetIcon.setImageResource(R.drawable.ic_share_generic_white)
listEmptyDatasetTitle.setText(R.string.members_search_failed)
listEmptyDatasetSubTitle.setText(R.string.members_search_empty)
}
}
}

fun AddMemberFragmentBinding.bindSelectedMember(member: OCMember) {
selectedMemberLayout.apply {
memberIcon.setImageResource(if (member.type == OCMemberType.GROUP) R.drawable.ic_group else R.drawable.ic_user)
memberName.text = member.displayName
memberRole.text = member.surname
}
}

fun AddMemberFragmentBinding.bindRoles(rolesAdapter: SpaceRolesAdapter, selectedRoleId: String?) {
selectedRoleId?.let {
inviteMemberButton.isEnabled = true
rolesAdapter.setSelectedRole(it)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
package com.owncloud.android.extensions

import android.app.AlertDialog
import android.app.DatePickerDialog
import android.content.Context
import android.content.DialogInterface
import android.icu.util.Calendar
import android.view.Menu
import android.view.MenuItem.SHOW_AS_ACTION_NEVER
import android.view.View
Expand All @@ -36,10 +38,15 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.snackbar.Snackbar
import com.owncloud.android.R
import com.owncloud.android.databinding.AddMemberFragmentBinding
import com.owncloud.android.domain.appregistry.model.AppRegistryProvider
import com.owncloud.android.utils.DisplayUtils
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone

fun Fragment.showErrorInSnackbar(genericErrorMessageId: Int, throwable: Throwable?) =
throwable?.let {
Expand Down Expand Up @@ -111,6 +118,61 @@ fun <T> Fragment.collectLatestLifecycleFlow(
}
}

fun Fragment.bindDatePickerDialog(
binding: AddMemberFragmentBinding,
expirationDate: String?,
onExpirationDateSelected: (String?) -> Unit,
) {
binding.expirationDateLayout.expirationDateSwitch.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
openDatePickerDialog(binding, expirationDate, onExpirationDateSelected)
} else {
binding.expirationDateLayout.expirationDateValue.visibility = View.GONE
onExpirationDateSelected(null)
}
}
}

fun Fragment.openDatePickerDialog(
binding: AddMemberFragmentBinding,
expirationDate: String?,
onExpirationDateSelected: (String?) -> Unit,
) {
val calendar = Calendar.getInstance()
val formatter = SimpleDateFormat(DisplayUtils.DATE_FORMAT_ISO, Locale.ROOT).apply {
timeZone = TimeZone.getTimeZone("UTC")
}

expirationDate?.let {
calendar.time = formatter.parse(it)
}

DatePickerDialog(
requireContext(),
{ _, selectedYear, selectedMonth, selectedDay ->
calendar.set(selectedYear, selectedMonth, selectedDay, 23, 59, 59)
calendar.set(Calendar.MILLISECOND, 999)
val isoExpirationDate = formatter.format(calendar.time)
onExpirationDateSelected(isoExpirationDate)
binding.expirationDateLayout.expirationDateValue.apply {
visibility = View.VISIBLE
text = DisplayUtils.displayDateToHumanReadable(isoExpirationDate)
}
},
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH)
).apply {
datePicker.minDate = Calendar.getInstance().timeInMillis
show()
setOnCancelListener {
if (expirationDate == null) {
binding.expirationDateLayout.expirationDateSwitch.isChecked = false
}
}
}
}

fun Fragment.addOpenInWebMenuOptions(
menu: Menu,
openInWebProviders: Map<String, Int> = emptyMap(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.owncloud.android.domain.exceptions.AccountNotTheSameException
import com.owncloud.android.domain.exceptions.BadOcVersionException
import com.owncloud.android.domain.exceptions.ConflictException
import com.owncloud.android.domain.exceptions.ConflictMemberException
import com.owncloud.android.domain.exceptions.ConflictShareException
import com.owncloud.android.domain.exceptions.CopyIntoDescendantException
import com.owncloud.android.domain.exceptions.CopyIntoSameFolderException
import com.owncloud.android.domain.exceptions.FileAlreadyExistsException
Expand Down Expand Up @@ -75,6 +76,7 @@ fun Throwable.parseError(
is BadOcVersionException -> resources.getString(R.string.auth_bad_oc_version_title)
is ConflictException -> resources.getString(R.string.error_conflict)
is ConflictMemberException -> resources.getString(R.string.members_add_conflict_error)
is ConflictShareException -> resources.getString(R.string.share_add_conflict_error)
is CopyIntoDescendantException -> resources.getString(R.string.copy_file_invalid_into_descendent)
is CopyIntoSameFolderException -> resources.getString(R.string.copy_file_invalid_overwrite)
is FileAlreadyExistsException -> resources.getString(R.string.file_already_exists)
Expand Down
Loading