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
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import eu.opencloud.android.domain.files.usecases.GetFolderContentUseCase
import eu.opencloud.android.domain.files.usecases.GetFolderImagesUseCase
import eu.opencloud.android.domain.files.usecases.GetPersonalRootFolderForAccountUseCase
import eu.opencloud.android.domain.files.usecases.GetSearchFolderContentUseCase
import eu.opencloud.android.domain.files.usecases.SearchFilesUseCase
import eu.opencloud.android.domain.files.usecases.GetSharedByLinkForAccountAsStreamUseCase
import eu.opencloud.android.domain.files.usecases.GetSharesRootFolderForAccount
import eu.opencloud.android.domain.files.usecases.GetWebDavUrlForSpaceUseCase
Expand Down Expand Up @@ -175,6 +176,7 @@ val useCaseModule = module {
factoryOf(::IsAnyFileAvailableLocallyAndNotAvailableOfflineUseCase)
factoryOf(::GetPersonalRootFolderForAccountUseCase)
factoryOf(::GetSearchFolderContentUseCase)
factoryOf(::SearchFilesUseCase)
factoryOf(::GetSharedByLinkForAccountAsStreamUseCase)
factoryOf(::GetSharesRootFolderForAccount)
factoryOf(::GetUrlToOpenInWebUseCase)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ val viewModelModule = module {
ShareViewModel(filePath, accountName, get(), get(), get(), get(), get(), get(), get(), get(), get(), get())
}
viewModel { (initialFolderToDisplay: OCFile, fileListOption: FileListOption) ->
MainFileListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
MainFileListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
initialFolderToDisplay, fileListOption)
}
viewModel { (ocFile: OCFile) -> ConflictsResolveViewModel(get(), get(), get(), get(), get(), ocFile) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,19 @@ class FileListAdapter(
var files = mutableListOf<Any>()
private var account: Account? = AccountUtils.getCurrentOpenCloudAccount(context)
private var fileListOption: FileListOption = FileListOption.ALL_FILES
private var isSearchActive: Boolean = false
private val disallowTouchesWithOtherWindows =
PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(context)

init {
setHasStableIds(true)
}

fun updateFileList(filesToAdd: List<OCFileWithSyncInfo>, fileListOption: FileListOption) {
fun updateFileList(
filesToAdd: List<OCFileWithSyncInfo>,
fileListOption: FileListOption,
isSearchActive: Boolean = false,
) {

val listWithFooter = mutableListOf<Any>()
listWithFooter.addAll(filesToAdd)
Expand All @@ -86,11 +91,17 @@ class FileListAdapter(
)
val diffResult = DiffUtil.calculateDiff(diffUtilCallback)

val searchStateChanged = this.isSearchActive != isSearchActive
files.clear()
files.addAll(listWithFooter)
this.fileListOption = fileListOption
this.isSearchActive = isSearchActive

diffResult.dispatchUpdatesTo(this)
if (searchStateChanged) {
notifyDataSetChanged()
} else {
diffResult.dispatchUpdatesTo(this)
}
}

override fun getItemId(position: Int): Long {
Expand Down Expand Up @@ -328,7 +339,10 @@ class FileListAdapter(
it.fileListLastMod.text = DisplayUtils.getRelativeTimestamp(context, file.modificationTimestamp)
it.threeDotMenu.isVisible = !hasActiveSelection
it.threeDotMenu.contentDescription = context.getString(R.string.content_description_file_operations, file.fileName)
if (fileListOption.isAvailableOffline() || (fileListOption.isSharedByLink() && fileWithSyncInfo.space == null)) {
val showSpacePath = fileListOption.isAvailableOffline() ||
(fileListOption.isSharedByLink() && fileWithSyncInfo.space == null) ||
isSearchActive
if (showSpacePath) {
it.spacePathLine.path.apply {
text = file.getParentRemotePath()
isVisible = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
Expand Down Expand Up @@ -185,6 +186,7 @@ class MainFileListFragment : Fragment(),
private var menu: Menu? = null
private var checkedFiles: List<OCFile> = emptyList()
private var filesToRemove: List<OCFile> = emptyList()
private var searchView: SearchView? = null
private var fileSingleFile: OCFile? = null
private var fileOptionsBottomSheetSingleFileLayout: LinearLayout? = null
private var succeededTransfers: List<OCTransfer>? = null
Expand Down Expand Up @@ -786,9 +788,11 @@ class MainFileListFragment : Fragment(),
collectLatestLifecycleFlow(mainFileListViewModel.fileListUiState) { fileListUiState ->
if (fileListUiState !is MainFileListViewModel.FileListUiState.Success) return@collectLatestLifecycleFlow

val isSearchActive = !fileListUiState.searchFilter.isNullOrBlank()
fileListAdapter.updateFileList(
filesToAdd = fileListUiState.folderContent,
fileListOption = fileListUiState.fileListOption,
isSearchActive = isSearchActive,
)
showOrHideEmptyView(fileListUiState)

Expand Down Expand Up @@ -928,7 +932,11 @@ class MainFileListFragment : Fragment(),
with(binding.emptyDataParent) {
root.isVisible = fileListUiState.folderContent.isEmpty()

if (fileListUiState.fileListOption.isSharedByLink() && fileListUiState.space != null) {
if (!fileListUiState.searchFilter.isNullOrBlank()) {
listEmptyDatasetIcon.setImageResource(R.drawable.ic_search)
listEmptyDatasetTitle.setText(R.string.local_file_list_search_with_no_matches)
listEmptyDatasetSubTitle.text = ""
} else if (fileListUiState.fileListOption.isSharedByLink() && fileListUiState.space != null) {
// Temporary solution for shares space
listEmptyDatasetIcon.setImageResource(R.drawable.ic_server_shares)
listEmptyDatasetTitle.setText(R.string.shares_list_empty_title)
Expand Down Expand Up @@ -1294,14 +1302,22 @@ class MainFileListFragment : Fragment(),
dialog.show(requireActivity().supportFragmentManager, DIALOG_CREATE_FOLDER)
}

override fun onQueryTextSubmit(query: String?): Boolean = false
override fun onQueryTextSubmit(query: String?): Boolean {
query?.let { mainFileListViewModel.updateSearchFilter(it) }
view?.findFocus()?.let {
val imm = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(it.windowToken, 0)
}
return true
}

override fun onQueryTextChange(newText: String?): Boolean {
newText?.let { mainFileListViewModel.updateSearchFilter(it) }
return true
}

fun setSearchListener(searchView: SearchView) {
this.searchView = searchView
searchView.setOnQueryTextListener(this)
}

Expand Down Expand Up @@ -1545,6 +1561,12 @@ class MainFileListFragment : Fragment(),
val ocFile = ocFileWithSyncInfo.file

if (ocFile.isFolder) {
searchView?.let {
if (!it.isIconified) {
it.setQuery("", false)
it.isIconified = true
}
}
mainFileListViewModel.updateFolderToDisplay(ocFile)
} else { // Click on a file
fileActions?.onFileClicked(ocFile)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import eu.opencloud.android.domain.files.usecases.GetFileByIdUseCase
import eu.opencloud.android.domain.files.usecases.GetFileByRemotePathUseCase
import eu.opencloud.android.domain.files.usecases.GetFolderContentAsStreamUseCase
import eu.opencloud.android.domain.files.usecases.GetSharedByLinkForAccountAsStreamUseCase
import eu.opencloud.android.domain.files.usecases.SearchFilesUseCase
import eu.opencloud.android.domain.files.usecases.SortFilesWithSyncInfoUseCase
import eu.opencloud.android.domain.spaces.model.OCSpace
import eu.opencloud.android.domain.spaces.usecases.GetSpaceWithSpecialsByIdForAccountUseCase
Expand All @@ -58,20 +59,24 @@ import eu.opencloud.android.providers.CoroutinesDispatcherProvider
import eu.opencloud.android.usecases.files.FilterFileMenuOptionsUseCase
import eu.opencloud.android.usecases.synchronization.SynchronizeFolderUseCase
import eu.opencloud.android.usecases.synchronization.SynchronizeFolderUseCase.SyncFolderMode.SYNC_CONTENTS
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import eu.opencloud.android.domain.files.usecases.SortType.Companion as SortTypeDomain

class MainFileListViewModel(
Expand All @@ -83,6 +88,7 @@ class MainFileListViewModel(
private val getSpaceWithSpecialsByIdForAccountUseCase: GetSpaceWithSpecialsByIdForAccountUseCase,
private val sortFilesWithSyncInfoUseCase: SortFilesWithSyncInfoUseCase,
private val synchronizeFolderUseCase: SynchronizeFolderUseCase,
private val searchFilesUseCase: SearchFilesUseCase,
getAppRegistryWhichAllowCreationAsStreamUseCase: GetAppRegistryWhichAllowCreationAsStreamUseCase,
private val getAppRegistryForMimeTypeAsStreamUseCase: GetAppRegistryForMimeTypeAsStreamUseCase,
private val getUrlToOpenInWebUseCase: GetUrlToOpenInWebUseCase,
Expand All @@ -99,6 +105,12 @@ class MainFileListViewModel(
val currentFolderDisplayed: MutableStateFlow<OCFile> = MutableStateFlow(initialFolderToDisplay)
val fileListOption: MutableStateFlow<FileListOption> = MutableStateFlow(fileListOptionParam)
private val searchFilter: MutableStateFlow<String> = MutableStateFlow("")

@OptIn(FlowPreview::class)
private val debouncedSearchFilter: Flow<String> = searchFilter.debounce { query ->
if (query.isBlank()) 0L else SEARCH_DEBOUNCE_MS
}
Comment on lines +109 to +112

private val sortTypeAndOrder = MutableStateFlow(Pair(SortType.SORT_TYPE_BY_NAME, SortOrder.SORT_ORDER_ASCENDING))
val space: MutableStateFlow<OCSpace?> = MutableStateFlow(null)
val appRegistryToCreateFiles: StateFlow<List<AppRegistryMimeType>> =
Expand All @@ -123,7 +135,7 @@ class MainFileListViewModel(
combine(
currentFolderDisplayed,
fileListOption,
searchFilter,
debouncedSearchFilter,
sortTypeAndOrder,
space,
) { currentFolderDisplayed, fileListOption, searchFilter, sortTypeAndOrder, space ->
Expand Down Expand Up @@ -211,7 +223,7 @@ class MainFileListViewModel(
viewModelScope.launch(coroutinesDispatcherProvider.io) {
val currentFolder = currentFolderDisplayed.value
val parentId = currentFolder.parentId
val parentDir: OCFile?
var parentDir: OCFile? = null

// browsing back to not shared by link or av offline should update to root
if (parentId != null && parentId != ROOT_PARENT_ID) {
Expand All @@ -225,8 +237,12 @@ class MainFileListViewModel(
FileListOption.SHARED_BY_LINK -> {
val fileById = fileByIdResult.getDataOrNull()
parentDir =
if (fileById != null && (!fileById.sharedByLink || fileById.sharedWithSharee != true) && fileById.spaceId == null) {
getFileByRemotePathUseCase(GetFileByRemotePathUseCase.Params(fileById.owner, ROOT_PATH)).getDataOrNull()
if (fileById != null && (!fileById.sharedByLink || fileById.sharedWithSharee != true) &&
fileById.spaceId == null
) {
getFileByRemotePathUseCase(
GetFileByRemotePathUseCase.Params(fileById.owner, ROOT_PATH)
).getDataOrNull()
} else {
fileById
}
Expand All @@ -235,14 +251,16 @@ class MainFileListViewModel(
FileListOption.AV_OFFLINE -> {
val fileById = fileByIdResult.getDataOrNull()
parentDir = if (fileById != null && (!fileById.isAvailableOffline)) {
getFileByRemotePathUseCase(GetFileByRemotePathUseCase.Params(fileById.owner, ROOT_PATH)).getDataOrNull()
getFileByRemotePathUseCase(
GetFileByRemotePathUseCase.Params(fileById.owner, ROOT_PATH)
).getDataOrNull()
} else {
fileById
}
}

FileListOption.SPACES_LIST -> {
parentDir = TODO("Move it to usecase if possible")
parentDir = null
}
}
} else if (parentId == ROOT_PARENT_ID) {
Expand All @@ -251,12 +269,35 @@ class MainFileListViewModel(
GetFileByRemotePathUseCase.Params(
remotePath = ROOT_PATH,
owner = currentFolder.owner,
spaceId = currentFolder.spaceId,
)
)
parentDir = rootFolderForAccountResult.getDataOrNull()
} else {
// Browsing to non existing parent folder.
TODO()
}

// Fallback: If parent was not resolved by ID (e.g. parentId was null, 0, or not found in DB)
if (parentDir == null) {
if (currentFolder.remotePath != ROOT_PATH) {
val parentRemotePath = currentFolder.getParentRemotePath()
parentDir = getFileByRemotePathUseCase(
GetFileByRemotePathUseCase.Params(
remotePath = parentRemotePath,
owner = currentFolder.owner,
spaceId = currentFolder.spaceId,
)
).getDataOrNull()
}

// If still null or at root, fallback to space/personal root folder
if (parentDir == null) {
parentDir = getFileByRemotePathUseCase(
GetFileByRemotePathUseCase.Params(
remotePath = ROOT_PATH,
owner = currentFolder.owner,
spaceId = currentFolder.spaceId,
)
).getDataOrNull()
}
}

parentDir?.let { updateFolderToDisplay(it) }
Expand Down Expand Up @@ -370,18 +411,60 @@ class MainFileListViewModel(
sortTypeAndOrder: Pair<SortType, SortOrder>,
space: OCSpace?,
): Flow<FileListUiState> =
when (fileListOption) {
FileListOption.ALL_FILES -> retrieveFlowForAllFiles(currentFolderDisplayed, currentFolderDisplayed.owner)
FileListOption.SHARED_BY_LINK -> retrieveFlowForShareByLink(currentFolderDisplayed, currentFolderDisplayed.owner)
FileListOption.AV_OFFLINE -> retrieveFlowForAvailableOffline(currentFolderDisplayed, currentFolderDisplayed.owner)
FileListOption.SPACES_LIST -> flowOf()
}.toFileListUiState(
currentFolderDisplayed,
fileListOption,
searchFilter,
sortTypeAndOrder,
space,
if (!searchFilter.isNullOrBlank()) {
retrieveFlowForSearch(
currentFolderDisplayed = currentFolderDisplayed,
fileListOption = fileListOption,
searchFilter = searchFilter,
sortTypeAndOrder = sortTypeAndOrder,
space = space,
)
} else {
when (fileListOption) {
FileListOption.ALL_FILES -> retrieveFlowForAllFiles(currentFolderDisplayed, currentFolderDisplayed.owner)
FileListOption.SHARED_BY_LINK -> retrieveFlowForShareByLink(currentFolderDisplayed, currentFolderDisplayed.owner)
FileListOption.AV_OFFLINE -> retrieveFlowForAvailableOffline(currentFolderDisplayed, currentFolderDisplayed.owner)
FileListOption.SPACES_LIST -> flowOf()
}.toFileListUiState(
currentFolderDisplayed,
fileListOption,
searchFilter,
sortTypeAndOrder,
space,
)
}

private fun retrieveFlowForSearch(
currentFolderDisplayed: OCFile,
fileListOption: FileListOption,
searchFilter: String,
sortTypeAndOrder: Pair<SortType, SortOrder>,
space: OCSpace?,
): Flow<FileListUiState> = flow {
emit(FileListUiState.Loading)
val searchResult = withContext(coroutinesDispatcherProvider.io) {
searchFilesUseCase(
SearchFilesUseCase.Params(
searchQuery = searchFilter,
accountName = currentFolderDisplayed.owner,
spaceId = null,
)
)
}
val filesWithSyncInfo = (searchResult.getDataOrNull() ?: emptyList())
.filter { showHiddenFiles || !it.file.fileName.startsWith(".") }
.let { sortList(it, sortTypeAndOrder) }

emit(
FileListUiState.Success(
folderToDisplay = currentFolderDisplayed,
folderContent = filesWithSyncInfo,
fileListOption = fileListOption,
searchFilter = searchFilter,
space = space,
)
)
}

private fun retrieveFlowForAllFiles(
currentFolderDisplayed: OCFile,
Expand Down Expand Up @@ -456,6 +539,7 @@ class MainFileListViewModel(

companion object {
private const val RECYCLER_VIEW_PREFERRED = "RECYCLER_VIEW_PREFERRED"
private const val SEARCH_DEBOUNCE_MS = 300L
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ import eu.opencloud.android.domain.exceptions.SSLRecoverablePeerUnverifiedExcept
import eu.opencloud.android.domain.exceptions.UnauthorizedException
import eu.opencloud.android.domain.files.model.FileListOption
import eu.opencloud.android.domain.files.model.OCFile
import eu.opencloud.android.domain.files.model.OCFile.Companion.ROOT_PARENT_ID
import eu.opencloud.android.domain.spaces.model.OCSpace
import eu.opencloud.android.domain.utils.Event
import eu.opencloud.android.extensions.checkPasscodeEnforced
Expand Down Expand Up @@ -785,7 +784,7 @@ class FileDisplayActivity : FileActivity(),
return
}
// If current file is root folder
else if (currentDirDisplayed.parentId == ROOT_PARENT_ID) {
else if (currentDirDisplayed.remotePath == OCFile.ROOT_PATH) {
// If current space is a project space or personal in a multi-personal account, navigate back to the spaces list
if (mainFileListFragment?.getCurrentSpace()?.isProject == true ||
(mainFileListFragment?.getCurrentSpace()?.isPersonal == true && isMultiPersonal)) {
Expand Down
Loading