Skip to content

Commit

Permalink
Feat: Add Swipe refresh to media (#985)
Browse files Browse the repository at this point in the history
* fix some exceptions

* add swipe to refresh media

* remove force refesh setting from media library settings

* fix window insets in bottom sheet

* run ktlintFormat
  • Loading branch information
anilbeesetti authored Jul 21, 2024
1 parent 6b38591 commit f6a51e3
Show file tree
Hide file tree
Showing 13 changed files with 260 additions and 164 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import java.io.FileWriter
import java.io.InputStreamReader
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.mozilla.universalchardet.UniversalDetector
Expand Down Expand Up @@ -204,38 +205,40 @@ fun Context.showToast(string: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, string, duration).show()
}

suspend fun Context.scanPaths(
paths: List<String>,
callback: ((String?, Uri?) -> Unit)? = null,
) = withContext(Dispatchers.IO) {
MediaScannerConnection.scanFile(
this@scanPaths,
paths.toTypedArray(),
arrayOf("video/*"),
callback,
)
suspend fun Context.scanPaths(paths: List<String>): Boolean = suspendCoroutine { continuation ->
try {
MediaScannerConnection.scanFile(
this@scanPaths,
paths.toTypedArray(),
arrayOf("video/*"),
) { path, uri ->
Log.d("ScanPath", "scanPaths: path=$path, uri=$uri")
continuation.resumeWith(Result.success(true))
}
} catch (e: Exception) {
continuation.resumeWith(Result.failure(e))
}
}

suspend fun Context.scanPath(file: File) {
withContext(Dispatchers.IO) {
if (file.isDirectory) {
file.listFiles()?.forEach { scanPath(it) }
} else {
scanPaths(listOf(file.path))
}
suspend fun Context.scanPath(file: File): Boolean {
return if (file.isDirectory) {
file.listFiles()?.all { scanPath(it) } ?: true
} else {
scanPaths(listOf(file.path))
}
}

suspend fun Context.scanStorage(callback: ((String?, Uri?) -> Unit)? = null) = withContext(Dispatchers.IO) {
val storagePath = Environment.getExternalStorageDirectory()?.path
suspend fun Context.scanStorage(
storagePath: String? = Environment.getExternalStorageDirectory()?.path,
): Boolean = withContext(Dispatchers.IO) {
if (storagePath != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
scanPaths(listOf(storagePath), callback)
return@withContext if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
scanPaths(listOf(storagePath))
} else {
scanPath(File(storagePath))
}
} else {
callback?.invoke(null, null)
false
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import dev.anilbeesetti.nextplayer.core.common.NextDispatchers
import dev.anilbeesetti.nextplayer.core.common.di.ApplicationScope
import dev.anilbeesetti.nextplayer.core.common.extensions.VIDEO_COLLECTION_URI
import dev.anilbeesetti.nextplayer.core.common.extensions.prettyName
import dev.anilbeesetti.nextplayer.core.common.extensions.scanPaths
import dev.anilbeesetti.nextplayer.core.common.extensions.scanStorage
import dev.anilbeesetti.nextplayer.core.database.converter.UriListConverter
import dev.anilbeesetti.nextplayer.core.database.dao.DirectoryDao
import dev.anilbeesetti.nextplayer.core.database.dao.MediumDao
Expand Down Expand Up @@ -44,6 +46,10 @@ class LocalMediaSynchronizer @Inject constructor(

private var mediaSyncingJob: Job? = null

override suspend fun refresh(path: String?): Boolean {
return path?.let { context.scanPaths(listOf(path)) } ?: context.scanStorage()
}

override fun startSync() {
if (mediaSyncingJob != null) return
mediaSyncingJob = getMediaVideosFlow().onEach { media ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package dev.anilbeesetti.nextplayer.core.media.sync

interface MediaSynchronizer {
suspend fun refresh(path: String? = null): Boolean
fun startSync()

fun stopSync()
}
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,11 @@ class PlayerActivity : AppCompatActivity() {
player.isPlaying &&
!isControlsLocked
) {
this.enterPictureInPictureMode(updatePictureInPictureParams())
try {
this.enterPictureInPictureMode(updatePictureInPictureParams())
} catch (e: IllegalStateException) {
e.printStackTrace()
}
}
}

Expand Down Expand Up @@ -513,7 +517,7 @@ class PlayerActivity : AppCompatActivity() {
}
}
pipButton.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPipSupported) {
this.enterPictureInPictureMode(updatePictureInPictureParams())
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,26 @@ val Activity.currentBrightness: Float

@Suppress("DEPRECATION")
fun Activity.prettyPrintIntent() {
Timber.apply {
d("* action: ${intent.action}")
d("* data: ${intent.data}")
d("* type: ${intent.type}")
d("* package: ${intent.`package`}")
d("* component: ${intent.component}")
d("* flags: ${intent.flags}")
intent.extras?.let { bundle ->
d("=== Extras ===")
bundle.keySet().forEachIndexed { i, key ->
buildString {
append("${i + 1}) $key: ")
bundle.get(key).let { append(if (it is Array<*>) Arrays.toString(it) else it) }
}.also { d(it) }
try {
Timber.apply {
d("* action: ${intent.action}")
d("* data: ${intent.data}")
d("* type: ${intent.type}")
d("* package: ${intent.`package`}")
d("* component: ${intent.component}")
d("* flags: ${intent.flags}")
intent.extras?.let { bundle ->
d("=== Extras ===")
bundle.keySet().forEachIndexed { i, key ->
buildString {
append("${i + 1}) $key: ")
bundle.get(key).let { append(if (it is Array<*>) Arrays.toString(it) else it) }
}.also { d(it) }
}
}
}
} catch (e: Exception) {
Timber.e(e)
e.printStackTrace()
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package dev.anilbeesetti.nextplayer.settings.screens.medialibrary

import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
Expand All @@ -18,22 +17,17 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.anilbeesetti.nextplayer.core.common.extensions.scanStorage
import dev.anilbeesetti.nextplayer.core.common.extensions.showToast
import dev.anilbeesetti.nextplayer.core.ui.R
import dev.anilbeesetti.nextplayer.core.ui.components.ClickablePreferenceItem
import dev.anilbeesetti.nextplayer.core.ui.components.NextTopAppBar
import dev.anilbeesetti.nextplayer.core.ui.components.PreferenceSwitch
import dev.anilbeesetti.nextplayer.core.ui.designsystem.NextIcons
import dev.anilbeesetti.nextplayer.settings.composables.PreferenceSubtitle
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3Api::class)
@Composable
Expand All @@ -45,12 +39,9 @@ fun MediaLibraryPreferencesScreen(
val preferences by viewModel.preferences.collectAsStateWithLifecycle()

val scrollBehaviour = TopAppBarDefaults.pinnedScrollBehavior()
val context = LocalContext.current
val scope = rememberCoroutineScope()

Scaffold(
modifier = Modifier
.nestedScroll(scrollBehaviour.nestedScrollConnection),
modifier = Modifier.nestedScroll(scrollBehaviour.nestedScrollConnection),
topBar = {
NextTopAppBar(
title = stringResource(id = R.string.media_library),
Expand Down Expand Up @@ -90,15 +81,6 @@ fun MediaLibraryPreferencesScreen(
HideFoldersSettings(
onClick = onFolderSettingClick,
)
ForceRescanStorageSetting(
onClick = {
scope.launch { context.scanStorage() }
context.showToast(
string = context.getString(R.string.scanning_storage),
duration = Toast.LENGTH_LONG,
)
},
)
}
}
}
Expand All @@ -115,18 +97,6 @@ fun HideFoldersSettings(
)
}

@Composable
fun ForceRescanStorageSetting(
onClick: () -> Unit,
) {
ClickablePreferenceItem(
title = stringResource(id = R.string.force_rescan_storage),
description = stringResource(id = R.string.force_rescan_storage_desc),
icon = NextIcons.Update,
onClick = onClick,
)
}

@Composable
fun MarkLastPlayedMediaSetting(
isChecked: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,25 @@ fun FoldersView(

when (foldersState) {
FoldersState.Loading -> CenterCircularProgressBar()
is FoldersState.Success -> if (foldersState.data.isEmpty()) {
NoVideosFound()
} else {
is FoldersState.Success -> {
MediaLazyList {
items(foldersState.data, key = { it.path }) {
FolderItem(
folder = it,
isRecentlyPlayedFolder = foldersState.recentPlayedVideo in it.mediaList,
preferences = preferences,
modifier = Modifier.combinedClickable(
onClick = { onFolderClick(it.path) },
onLongClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
showFolderActionsFor = it
},
),
)
if (foldersState.data.isEmpty()) {
item { NoVideosFound() }
} else {
items(foldersState.data, key = { it.path }) {
FolderItem(
folder = it,
isRecentlyPlayedFolder = foldersState.recentPlayedVideo in it.mediaList,
preferences = preferences,
modifier = Modifier.combinedClickable(
onClick = { onFolderClick(it.path) },
onLongClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
showFolderActionsFor = it
},
),
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
Expand Down Expand Up @@ -75,7 +76,12 @@ fun CenterCircularProgressBar() {
@Composable
fun NoVideosFound() {
Column(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = 20.dp,
vertical = 40.dp,
),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Expand Down Expand Up @@ -141,17 +147,9 @@ fun OptionsBottomSheet(
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
windowInsets = WindowInsets(0),
windowInsets = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom),
) {
Column(
modifier = Modifier
.padding(
bottom = WindowInsets.navigationBars
.asPaddingValues()
.calculateBottomPadding(),
)
.padding(bottom = 8.dp),
) {
Column(modifier = Modifier.padding(bottom = 8.dp)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,26 +71,28 @@ fun VideosView(

when (videosState) {
VideosState.Loading -> CenterCircularProgressBar()
is VideosState.Success -> if (videosState.data.isEmpty()) {
NoVideosFound()
} else {
is VideosState.Success -> {
MediaLazyList {
items(videosState.data, key = { it.path }) { video ->
LaunchedEffect(Unit) {
onVideoLoaded(Uri.parse(video.uriString))
if (videosState.data.isEmpty()) {
item { NoVideosFound() }
} else {
items(videosState.data, key = { it.path }) { video ->
LaunchedEffect(Unit) {
onVideoLoaded(Uri.parse(video.uriString))
}
VideoItem(
video = video,
preferences = preferences,
isRecentlyPlayedVideo = video == videosState.recentPlayedVideo,
modifier = Modifier.combinedClickable(
onClick = { onVideoClick(Uri.parse(video.uriString)) },
onLongClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
showMediaActionsFor = video
},
),
)
}
VideoItem(
video = video,
preferences = preferences,
isRecentlyPlayedVideo = video == videosState.recentPlayedVideo,
modifier = Modifier.combinedClickable(
onClick = { onVideoClick(Uri.parse(video.uriString)) },
onLongClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
showMediaActionsFor = video
},
),
)
}
}
}
Expand Down
Loading

0 comments on commit f6a51e3

Please sign in to comment.