Skip to content
Closed
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
11 changes: 8 additions & 3 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
[*.{kt,kts}]
max_line_length = off
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ktlint_code_style = android_studio
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true
ktlint_standard_property-naming = disabled
ktlint_function_naming_ignore_when_annotated_with = Composable

[build/**]
ktlint = disabled

[**/build/**]
ktlint = disabled
4 changes: 3 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ android {
splits {
abi {
//noinspection WrongGradleMethod
val isBuildingBundle = gradle.startParameter.taskNames.any { it.lowercase().contains("bundle") }
val isBuildingBundle = gradle.startParameter.taskNames.any {
it.lowercase().contains("bundle")
}

isEnable = !isBuildingBundle
reset()
Expand Down
59 changes: 31 additions & 28 deletions app/src/main/java/dev/anilbeesetti/nextplayer/ImageLoaderModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dev.anilbeesetti.nextplayer.core.data.repository.PreferencesRepository
import dev.anilbeesetti.nextplayer.core.model.ThumbnailGenerationStrategy
import okio.FileSystem
import javax.inject.Singleton
import okio.FileSystem

@Module
@InstallIn(SingletonComponent::class)
Expand All @@ -25,31 +25,34 @@ object ImageLoaderModule {
fun provideImageLoader(
@ApplicationContext context: Context,
preferencesRepository: PreferencesRepository,
): ImageLoader {
return ImageLoader.Builder(context)
.components {
add(
VideoThumbnailDecoder.Factory(
thumbnailStrategy = {
val preferences = preferencesRepository.applicationPreferences.value
when (preferences.thumbnailGenerationStrategy) {
ThumbnailGenerationStrategy.FIRST_FRAME -> ThumbnailStrategy.FirstFrame
ThumbnailGenerationStrategy.FRAME_AT_PERCENTAGE -> ThumbnailStrategy.FrameAtPercentage(preferences.thumbnailFramePosition)
ThumbnailGenerationStrategy.HYBRID -> ThumbnailStrategy.Hybrid(preferences.thumbnailFramePosition)
}
},
),
)
}
.diskCachePolicy(CachePolicy.ENABLED)
.diskCache(
DiskCache.Builder()
.fileSystem(FileSystem.SYSTEM)
.directory(context.filesDir.resolve("thumbnails"))
.maxSizePercent(1.0)
.build(),
): ImageLoader = ImageLoader.Builder(context)
.components {
add(
VideoThumbnailDecoder.Factory(
thumbnailStrategy = {
val preferences = preferencesRepository.applicationPreferences.value
when (preferences.thumbnailGenerationStrategy) {
ThumbnailGenerationStrategy.FIRST_FRAME -> ThumbnailStrategy.FirstFrame
ThumbnailGenerationStrategy.FRAME_AT_PERCENTAGE ->
ThumbnailStrategy.FrameAtPercentage(
preferences.thumbnailFramePosition,
)
ThumbnailGenerationStrategy.HYBRID -> ThumbnailStrategy.Hybrid(
preferences.thumbnailFramePosition,
)
}
},
),
)
.crossfade(true)
.build()
}
}
}
.diskCachePolicy(CachePolicy.ENABLED)
.diskCache(
DiskCache.Builder()
.fileSystem(FileSystem.SYSTEM)
.directory(context.filesDir.resolve("thumbnails"))
.maxSizePercent(1.0)
.build(),
)
.crossfade(true)
.build()
}
14 changes: 4 additions & 10 deletions app/src/main/java/dev/anilbeesetti/nextplayer/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ import dev.anilbeesetti.nextplayer.core.ui.theme.NextPlayerTheme
import dev.anilbeesetti.nextplayer.navigation.MediaRootRoute
import dev.anilbeesetti.nextplayer.navigation.mediaNavGraph
import dev.anilbeesetti.nextplayer.navigation.settingsNavGraph
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.launch

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
Expand Down Expand Up @@ -160,9 +160,7 @@ class MainActivity : ComponentActivity() {
* current system context.
*/
@Composable
fun shouldUseDarkTheme(
uiState: MainActivityUiState,
): Boolean = when (uiState) {
fun shouldUseDarkTheme(uiState: MainActivityUiState): Boolean = when (uiState) {
MainActivityUiState.Loading -> isSystemInDarkTheme()
is MainActivityUiState.Success -> when (uiState.preferences.themeConfig) {
ThemeConfig.SYSTEM -> isSystemInDarkTheme()
Expand All @@ -172,9 +170,7 @@ fun shouldUseDarkTheme(
}

@Composable
fun shouldUseHighContrastDarkTheme(
uiState: MainActivityUiState,
): Boolean = when (uiState) {
fun shouldUseHighContrastDarkTheme(uiState: MainActivityUiState): Boolean = when (uiState) {
MainActivityUiState.Loading -> false
is MainActivityUiState.Success -> uiState.preferences.useHighContrastDarkTheme
}
Expand All @@ -183,9 +179,7 @@ fun shouldUseHighContrastDarkTheme(
* Returns `true` if the dynamic color is disabled, as a function of the [uiState].
*/
@Composable
fun shouldUseDynamicTheming(
uiState: MainActivityUiState,
): Boolean = when (uiState) {
fun shouldUseDynamicTheming(uiState: MainActivityUiState): Boolean = when (uiState) {
MainActivityUiState.Loading -> false
is MainActivityUiState.Success -> uiState.preferences.useDynamicColors
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

@HiltViewModel
class MainViewModel @Inject constructor(
private val preferencesRepository: PreferencesRepository,
) : ViewModel() {
class MainViewModel @Inject constructor(private val preferencesRepository: PreferencesRepository) :
ViewModel() {

val uiState = preferencesRepository.applicationPreferences.map { preferences ->
MainActivityUiState.Success(preferences)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope

@HiltAndroidApp
class NextPlayerApplication : Application(), SingletonImageLoader.Factory {
class NextPlayerApplication :
Application(),
SingletonImageLoader.Factory {

@Inject
lateinit var preferencesRepository: PreferencesRepository
Expand All @@ -27,7 +29,9 @@ class NextPlayerApplication : Application(), SingletonImageLoader.Factory {

override fun onCreate() {
super.onCreate()
Thread.setDefaultUncaughtExceptionHandler(GlobalExceptionHandler(applicationContext, CrashActivity::class.java))
Thread.setDefaultUncaughtExceptionHandler(
GlobalExceptionHandler(applicationContext, CrashActivity::class.java),
)
}

override fun newImageLoader(context: PlatformContext): ImageLoader = imageLoader
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,29 @@ import android.os.Build.VERSION.SDK_INT
import androidx.core.graphics.applyCanvas
import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.toDrawable
import androidx.core.graphics.get
import coil3.ImageLoader
import coil3.annotation.ExperimentalCoilApi
import coil3.asImage
import coil3.decode.ContentMetadata
import coil3.decode.DecodeResult
import coil3.decode.DecodeUtils
import coil3.decode.Decoder
import coil3.decode.ImageSource
import coil3.disk.DiskCache
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import coil3.toAndroidUri
import okio.FileSystem
import androidx.core.graphics.get
import io.github.anilbeesetti.nextlib.mediainfo.MediaThumbnailRetriever
import kotlin.math.abs
import coil3.decode.DecodeUtils
import coil3.request.maxBitmapSize
import coil3.size.Precision
import coil3.size.Size
import coil3.size.pxOrElse
import coil3.toAndroidUri
import coil3.util.component1
import coil3.util.component2
import io.github.anilbeesetti.nextlib.mediainfo.MediaThumbnailRetriever
import kotlin.math.abs
import kotlin.math.roundToInt
import okio.FileSystem

class VideoThumbnailDecoder(
private val source: ImageSource,
Expand Down Expand Up @@ -66,7 +66,7 @@ class VideoThumbnailDecoder(

// Cache is sufficient only if requested size <= cached size (or size is unspecified)
val cacheIsSufficient = (requestedWidth == 0 || requestedWidth <= cachedWidth) &&
(requestedHeight == 0 || requestedHeight <= cachedHeight)
(requestedHeight == 0 || requestedHeight <= cachedHeight)

if (cacheIsSufficient) {
val dstSize = computeDstSize(cachedWidth, cachedHeight)
Expand Down Expand Up @@ -98,7 +98,8 @@ class VideoThumbnailDecoder(
ffmpegRetriever.setDataSource(source)

// First, try to get embedded picture (album art/metadata thumbnail)
val embeddedPicture = nativeRetriever.embeddedPicture ?: ffmpegRetriever.getEmbeddedPicture()
val embeddedPicture =
nativeRetriever.embeddedPicture ?: ffmpegRetriever.getEmbeddedPicture()
val embeddedPictureBitmap = embeddedPicture?.let { pictureBytes ->
BitmapFactory.decodeByteArray(pictureBytes, 0, pictureBytes.size)
}
Expand All @@ -116,14 +117,16 @@ class VideoThumbnailDecoder(

is ThumbnailStrategy.FrameAtPercentage -> {
val timeUs = (videoDuration * strategy.percentage * 1000).toLong()
nativeRetriever.getFrameAtTime(timeUs) ?: ffmpegRetriever.getFrameAtTime(timeUs)
nativeRetriever.getFrameAtTime(timeUs)
?: ffmpegRetriever.getFrameAtTime(timeUs)
}

is ThumbnailStrategy.Hybrid -> {
val firstFrame = nativeRetriever.getFrameAtTime(0)
if (firstFrame == null || isSolidColor(firstFrame)) {
val timeUs = (videoDuration * strategy.percentage * 1000).toLong()
nativeRetriever.getFrameAtTime(timeUs) ?: ffmpegRetriever.getFrameAtTime(timeUs)
nativeRetriever.getFrameAtTime(timeUs)
?: ffmpegRetriever.getFrameAtTime(timeUs)
} else {
firstFrame
}
Expand Down Expand Up @@ -187,13 +190,12 @@ class VideoThumbnailDecoder(
}
}

private fun readFromDiskCache(): DiskCache.Snapshot? {
return if (options.diskCachePolicy.readEnabled) {
private fun readFromDiskCache(): DiskCache.Snapshot? =
if (options.diskCachePolicy.readEnabled) {
diskCache.value?.openSnapshot(diskCacheKey)
} else {
null
}
}

private fun writeToDiskCache(inBitmap: Bitmap) {
if (!options.diskCachePolicy.writeEnabled) return
Expand All @@ -208,7 +210,12 @@ class VideoThumbnailDecoder(
}
}

private fun normalizeBitmap(inBitmap: Bitmap, srcWidth: Int, srcHeight: Int, dstSize: Size): Bitmap {
private fun normalizeBitmap(
inBitmap: Bitmap,
srcWidth: Int,
srcHeight: Int,
dstSize: Size,
): Bitmap {
val scale = DecodeUtils.computeSizeMultiplier(
srcWidth = srcWidth,
srcHeight = srcHeight,
Expand All @@ -224,7 +231,8 @@ class VideoThumbnailDecoder(
val dstHeight = (scale * inBitmap.height).roundToInt()

val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
val outBitmap = createBitmap(dstWidth, dstHeight, inBitmap.config ?: Bitmap.Config.ARGB_8888)
val outBitmap =
createBitmap(dstWidth, dstHeight, inBitmap.config ?: Bitmap.Config.ARGB_8888)
outBitmap.applyCanvas {
scale(scale, scale)
drawBitmap(inBitmap, 0f, 0f, paint)
Expand Down Expand Up @@ -263,9 +271,7 @@ class VideoThumbnailDecoder(
)
}

class Factory(
private val thumbnailStrategy: () -> ThumbnailStrategy,
) : Decoder.Factory {
class Factory(private val thumbnailStrategy: () -> ThumbnailStrategy) : Decoder.Factory {
override fun create(
result: SourceFetchResult,
options: Options,
Expand All @@ -280,9 +286,8 @@ class VideoThumbnailDecoder(
)
}

private fun isApplicable(mimeType: String?): Boolean {
return mimeType != null && mimeType.startsWith("video/")
}
private fun isApplicable(mimeType: String?): Boolean =
mimeType != null && mimeType.startsWith("video/")
}
}

Expand Down Expand Up @@ -358,8 +363,8 @@ private fun isSolidColor(bitmap: Bitmap, threshold: Float = 0.7f): Boolean {
val b = color and 0xFF

abs(r - referenceR) <= tolerance &&
abs(g - referenceG) <= tolerance &&
abs(b - referenceB) <= tolerance
abs(g - referenceG) <= tolerance &&
abs(b - referenceB) <= tolerance
}

val similarityRatio = similarCount.toFloat() / sampledColors.size
Expand Down
Loading