diff --git a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/filebrowser/FileBrowserFragment.kt b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/filebrowser/FileBrowserFragment.kt index b6f49dadc..96712544f 100644 --- a/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/filebrowser/FileBrowserFragment.kt +++ b/app/src/main/java/uk/co/sentinelweb/cuer/app/ui/filebrowser/FileBrowserFragment.kt @@ -19,7 +19,7 @@ import uk.co.sentinelweb.cuer.app.ui.common.ktx.bindFlow import uk.co.sentinelweb.cuer.app.ui.common.navigation.* import uk.co.sentinelweb.cuer.app.ui.common.navigation.NavigationModel.Param.BACK_PARAMS import uk.co.sentinelweb.cuer.app.ui.common.navigation.NavigationModel.Target.NAV_BACK -import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesContract.Label +import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesContract.Effect import uk.co.sentinelweb.cuer.app.ui.play_control.CompactPlayerScroll import uk.co.sentinelweb.cuer.app.ui.player.PlayerContract import uk.co.sentinelweb.cuer.app.util.extension.fragmentScopeWithSource @@ -90,18 +90,18 @@ class FileBrowserFragment : Fragment(), AndroidScopeComponent { ) } statusBarColor.setStatusBarColorResource(R.color.black) - bindFlow(viewModel.labels, ::observeLabels) + bindFlow(viewModel.effects, ::observeLabels) remoteIdArg?.apply { viewModel.init(this, filePathArg) } } - private fun observeLabels(label: Label) { + private fun observeLabels(label: Effect) { when (label) { - Label.Init -> {} - Label.Up -> { + Effect.Init -> {} + Effect.Up -> { navRouter.navigate(NavigationModel(NAV_BACK, mapOf(BACK_PARAMS to R.id.navigation_remotes))) } - Label.Settings -> navigationProvider.navigate(R.id.navigation_settings_root) + Effect.Settings -> navigationProvider.navigate(R.id.navigation_settings_root) else -> Unit } } @@ -135,7 +135,7 @@ class FileBrowserFragment : Fragment(), AndroidScopeComponent { scope(named()) { viewModel { FilesViewModel( - state = FilesContract.State(), + initialState = FilesContract.State(), filesInteractor = get(), remotesRepository = get(), mapper = get(), diff --git a/app/src/main/java/uk/co/sentinelweb/cuer/app/work/worker/UpcomingVideosCheckWorker.kt b/app/src/main/java/uk/co/sentinelweb/cuer/app/work/worker/UpcomingVideosCheckWorker.kt index db3a7c63c..f148b8465 100644 --- a/app/src/main/java/uk/co/sentinelweb/cuer/app/work/worker/UpcomingVideosCheckWorker.kt +++ b/app/src/main/java/uk/co/sentinelweb/cuer/app/work/worker/UpcomingVideosCheckWorker.kt @@ -29,9 +29,8 @@ class UpcomingVideosCheckWorker( Result.failure() } - companion object { val WORK_NAME = "UpcomingVideosCheck" val MINS_CHECK = 30 } -} \ No newline at end of file +} diff --git a/database/build.gradle.kts b/database/build.gradle.kts index 31f115cd3..649ad3c50 100644 --- a/database/build.gradle.kts +++ b/database/build.gradle.kts @@ -2,7 +2,6 @@ plugins { kotlin("multiplatform") kotlin("native.cocoapods") id("com.android.library") -// kotlin("com.android.application") id("com.squareup.sqldelight") kotlin("plugin.serialization") } @@ -10,7 +9,6 @@ plugins { group = "uk.co.sentinelweb.cuer" version = "1.0" -//val ver_jvm: String by project val ver_coroutines: String by project val ver_kotlinx_serialization_core: String by project val ver_sqldelight: String by project @@ -19,7 +17,6 @@ val ver_koin: String by project val ver_turbine: String by project val ver_kotlin_fixture: String by project val ver_mockk: String by project - val ver_swift_tools: String by project val ver_ios_deploy_target: String by project diff --git a/database/src/androidUnitTest/kotlin/uk/co/sentinelweb/cuer/db/repository/SqldelightMediaDatabaseRepositoryTest.kt b/database/src/androidUnitTest/kotlin/uk/co/sentinelweb/cuer/db/repository/SqldelightMediaDatabaseRepositoryTest.kt index 8461da475..786ae26a1 100644 --- a/database/src/androidUnitTest/kotlin/uk/co/sentinelweb/cuer/db/repository/SqldelightMediaDatabaseRepositoryTest.kt +++ b/database/src/androidUnitTest/kotlin/uk/co/sentinelweb/cuer/db/repository/SqldelightMediaDatabaseRepositoryTest.kt @@ -14,8 +14,8 @@ import org.koin.test.inject import uk.co.sentinelweb.cuer.app.db.Database import uk.co.sentinelweb.cuer.app.db.repository.ChannelDatabaseRepository import uk.co.sentinelweb.cuer.app.db.repository.ConflictException -import uk.co.sentinelweb.cuer.app.db.repository.MediaDatabaseRepository import uk.co.sentinelweb.cuer.app.db.repository.DbResult +import uk.co.sentinelweb.cuer.app.db.repository.MediaDatabaseRepository import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Filter.* import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Operation.DELETE import uk.co.sentinelweb.cuer.app.orchestrator.OrchestratorContract.Operation.FULL @@ -148,6 +148,7 @@ class SqldelightMediaDatabaseRepositoryTest : KoinTest { channelData = initialSaved.channelData, thumbNail = initialSaved.thumbNail, image = initialSaved.image, + length = initialSaved.length, broadcastDate = null, ) val updated = sut.save(changed, emit = true, flat = false).data!! @@ -178,6 +179,7 @@ class SqldelightMediaDatabaseRepositoryTest : KoinTest { ), thumbNail = media.thumbNail?.copy(id = saved[i].thumbNail?.id), image = media.image?.copy(id = saved[i].image?.id), + length = media.length, ) } @@ -207,6 +209,7 @@ class SqldelightMediaDatabaseRepositoryTest : KoinTest { thumbNail = it.thumbNail, image = it.image, broadcastDate = null, + length = it.length, ) } val updated = sut.save(changed, emit = true, flat = false).data!! diff --git a/database/src/commonMain/kotlin/uk/co/sentinelweb/cuer/db/factory/DatabaseFactory.kt b/database/src/commonMain/kotlin/uk/co/sentinelweb/cuer/db/factory/DatabaseFactory.kt index 1703bf8be..9156a0817 100644 --- a/database/src/commonMain/kotlin/uk/co/sentinelweb/cuer/db/factory/DatabaseFactory.kt +++ b/database/src/commonMain/kotlin/uk/co/sentinelweb/cuer/db/factory/DatabaseFactory.kt @@ -47,16 +47,10 @@ class DatabaseFactory( ) ).apply { log.d("Database.Schema.version ${Database.Schema.version} prefs.dbVersion ${prefs.dbVersion}") -// }.apply { -// Database.Schema.migrate( -// driver = driver, -// oldVersion = prefs.dbVersion, -// newVersion = Database.Schema.version, -// ) }.also { prefs.dbVersion = Database.Schema.version }.apply { log.d("Database.Schema.version ${Database.Schema.version} prefs.dbVersion ${prefs.dbVersion}") } } -} \ No newline at end of file +} diff --git a/database/src/commonMain/kotlin/uk/co/sentinelweb/cuer/db/mapper/MediaMapper.kt b/database/src/commonMain/kotlin/uk/co/sentinelweb/cuer/db/mapper/MediaMapper.kt index 70557a4d2..6aa8f7a2e 100644 --- a/database/src/commonMain/kotlin/uk/co/sentinelweb/cuer/db/mapper/MediaMapper.kt +++ b/database/src/commonMain/kotlin/uk/co/sentinelweb/cuer/db/mapper/MediaMapper.kt @@ -39,7 +39,8 @@ class MediaMapper( isLiveBroadcast = entity.flags.hasFlag(FLAG_LIVE), isLiveBroadcastUpcoming = entity.flags.hasFlag(FLAG_LIVE_UPCOMING), playFromStart = entity.flags.hasFlag(FLAG_PLAY_FROM_START), - broadcastDate = entity.broadcast_date + broadcastDate = entity.broadcast_date, + length = entity.length ) fun map(domain: MediaDomain): Media = Media( @@ -58,7 +59,8 @@ class MediaMapper( thumb_id = domain.thumbNail?.id?.id?.value, image_id = domain.image?.id?.id?.value, flags = mapFlags(domain), - broadcast_date = domain.broadcastDate + broadcast_date = domain.broadcastDate, + length = domain.length ) private fun mapFlags(domain: MediaDomain):Long = @@ -69,4 +71,4 @@ class MediaMapper( FLAG_LIVE_UPCOMING to domain.isLiveBroadcastUpcoming, FLAG_PLAY_FROM_START to domain.playFromStart ) -} \ No newline at end of file +} diff --git a/database/src/commonMain/sqldelight/uk/co/sentinelweb/cuer/database/entity/MediaEntity.sq b/database/src/commonMain/sqldelight/uk/co/sentinelweb/cuer/database/entity/MediaEntity.sq index 0989e359b..984552127 100644 --- a/database/src/commonMain/sqldelight/uk/co/sentinelweb/cuer/database/entity/MediaEntity.sq +++ b/database/src/commonMain/sqldelight/uk/co/sentinelweb/cuer/database/entity/MediaEntity.sq @@ -17,6 +17,7 @@ CREATE TABLE media ( thumb_id TEXT, image_id TEXT, broadcast_date TEXT AS kotlinx.datetime.LocalDateTime DEFAULT NULL, + length INTEGER, FOREIGN KEY (channel_id) REFERENCES channel(id) ); @@ -34,7 +35,7 @@ CREATE INDEX media_description_index ON media(description); create: INSERT INTO media ( id, flags, type, url, title, duration, position, date_last_played, description, platform, platform_id, -published, channel_id, thumb_id, image_id, broadcast_date ) VALUES ? +published, channel_id, thumb_id, image_id, broadcast_date, length ) VALUES ? --ON CONFLICT (platform, platform_id) DO NOTHING ; @@ -43,7 +44,7 @@ published, channel_id, thumb_id, image_id, broadcast_date ) VALUES ? update: REPLACE INTO media ( id, flags, type, url, title, duration, position, date_last_played, description, platform, platform_id, -published, channel_id, thumb_id, image_id, broadcast_date ) VALUES ? +published, channel_id, thumb_id, image_id, broadcast_date, length ) VALUES ? -- ON CONFLICT (platform, platform_id) DO NOTHING ; diff --git a/database/src/commonMain/sqldelight/uk/co/sentinelweb/cuer/database/migrations/2.sqm b/database/src/commonMain/sqldelight/uk/co/sentinelweb/cuer/database/migrations/2.sqm new file mode 100644 index 000000000..b1e34e1a2 --- /dev/null +++ b/database/src/commonMain/sqldelight/uk/co/sentinelweb/cuer/database/migrations/2.sqm @@ -0,0 +1 @@ +ALTER TABLE media ADD COLUMN length INTEGER; diff --git a/database/src/commonMain/sqldelight/uk/co/sentinelweb/cuer/database/verify/2.db b/database/src/commonMain/sqldelight/uk/co/sentinelweb/cuer/database/verify/2.db deleted file mode 100644 index 3d118bc2b..000000000 Binary files a/database/src/commonMain/sqldelight/uk/co/sentinelweb/cuer/database/verify/2.db and /dev/null differ diff --git a/domain/src/androidMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/FileSizeMapper.kt b/domain/src/androidMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/FileSizeMapper.kt new file mode 100644 index 000000000..cbf8914a0 --- /dev/null +++ b/domain/src/androidMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/FileSizeMapper.kt @@ -0,0 +1,27 @@ +package uk.co.sentinelweb.cuer.core.mappers + +actual class FileSizeMapper { + actual fun formatFileSize(sizeBytes: Long): String = + formatFileSize(bytes = sizeBytes) +} + +/** + * Formats file size in bytes to a human-readable string with appropriate unit (G, M, K). + * @param bytes The file size in bytes + * @param decimalPlaces Number of decimal places to include (default: 1) + * @return Formatted string representation of the file size + */ +fun formatFileSize(bytes: Long, decimalPlaces: Int = 1): String { + if (bytes <= 0) return "-" + + val units = arrayOf("", "K", "M", "G", "T") + val digitGroups = (Math.log10(bytes.toDouble()) / Math.log10(1024.0)).toInt() + + val format = if (decimalPlaces > 0) "%.${decimalPlaces}f %s" else "%.0f %s" + + return String.format( + format, + bytes / Math.pow(1024.0, digitGroups.toDouble()), + units[digitGroups.coerceAtMost(units.size - 1)] + ).trim() +} diff --git a/domain/src/androidUnitTest/kotlin/uk/co/sentinelweb/cuer/core/mappers/FileSizeMapperTest.kt b/domain/src/androidUnitTest/kotlin/uk/co/sentinelweb/cuer/core/mappers/FileSizeMapperTest.kt new file mode 100644 index 000000000..e90a96e84 --- /dev/null +++ b/domain/src/androidUnitTest/kotlin/uk/co/sentinelweb/cuer/core/mappers/FileSizeMapperTest.kt @@ -0,0 +1,23 @@ +package uk.co.sentinelweb.cuer.core.mappers + +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class FileSizeMapperTest { + private val sut = FileSizeMapper() + + @Before + fun setUp() { + + } + + @Test + fun formatFileSize() { + assertEquals("1.0", sut.formatFileSize(1)) + assertEquals("1.0 K", sut.formatFileSize(1024)) + assertEquals("1.0 M", sut.formatFileSize(1024 * 1024)) + assertEquals("1.0 G", sut.formatFileSize(1024 * 1024 * 1024)) + } + +} diff --git a/domain/src/androidUnitTest/kotlin/uk/co/sentinelweb/cuer/core/mappers/TimeSinceFormatterTest.kt b/domain/src/androidUnitTest/kotlin/uk/co/sentinelweb/cuer/core/mappers/TimeSinceFormatterTest.kt index 7d7a043b5..62af65a14 100644 --- a/domain/src/androidUnitTest/kotlin/uk/co/sentinelweb/cuer/core/mappers/TimeSinceFormatterTest.kt +++ b/domain/src/androidUnitTest/kotlin/uk/co/sentinelweb/cuer/core/mappers/TimeSinceFormatterTest.kt @@ -53,7 +53,7 @@ class TimeSinceFormatterTest { @Test fun formatTimeSince_months() { - assertEquals("6mth", sut.formatTimeSince(baseTime - 1000L * 60 * 60 * 24 * 200)) + assertEquals("6mo", sut.formatTimeSince(baseTime - 1000L * 60 * 60 * 24 * 200)) } @Test @@ -89,7 +89,7 @@ class TimeSinceFormatterTest { @Test fun formatTimeShort_months() { - assertEquals("6mth", sut.formatTimeShort(1000L * 60 * 60 * 24 * 200)) + assertEquals("6mo", sut.formatTimeShort(1000L * 60 * 60 * 24 * 200)) } @Test @@ -102,4 +102,4 @@ class TimeSinceFormatterTest { assertEquals("--", sut.formatTimeShort(1000L * 60 * 60 * 24 * 365 * 21)) } -} \ No newline at end of file +} diff --git a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/ExpectedMappers.kt b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/ExpectedMappers.kt index 039a7db6a..70ad66245 100644 --- a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/ExpectedMappers.kt +++ b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/ExpectedMappers.kt @@ -20,6 +20,10 @@ expect class DateTimeFormatter() { fun formatDateTimeNullable(dateTime: LocalDateTime?): String } +expect class FileSizeMapper { + fun formatFileSize(sizeBytes: Long): String +} + fun String.strip00() = this.let { var formatted = it while (formatted.startsWith("00:")) { diff --git a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/TimeSinceFormatter.kt b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/TimeSinceFormatter.kt index f58c0ab89..4d6cad95a 100644 --- a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/TimeSinceFormatter.kt +++ b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/TimeSinceFormatter.kt @@ -21,7 +21,7 @@ class TimeSinceFormatter( differenceSeconds < SEC_TO_HOURS -> "${differenceSeconds / SEC_TO_MIN}m" differenceSeconds < SEC_TO_DAYS -> "${differenceSeconds / SEC_TO_HOURS}h" differenceSeconds < SEC_TO_MONTHS -> "${differenceSeconds / SEC_TO_DAYS}d" - differenceSeconds < SEC_TO_YEARS -> "${differenceSeconds / SEC_TO_MONTHS}mth" + differenceSeconds < SEC_TO_YEARS -> "${differenceSeconds / SEC_TO_MONTHS}mo" differenceSeconds < SEC_TO_YEARS * 20 -> "${differenceSeconds / SEC_TO_YEARS}y" else -> "--" } @@ -37,7 +37,7 @@ class TimeSinceFormatter( positiveSeconds < SEC_TO_HOURS -> "$sign${positiveSeconds / SEC_TO_MIN}m" positiveSeconds < SEC_TO_DAYS -> "$sign${positiveSeconds / SEC_TO_HOURS}h" positiveSeconds < SEC_TO_MONTHS -> "$sign${positiveSeconds / SEC_TO_DAYS}d" - positiveSeconds < SEC_TO_YEARS -> "$sign${positiveSeconds / SEC_TO_MONTHS}mth" + positiveSeconds < SEC_TO_YEARS -> "$sign${positiveSeconds / SEC_TO_MONTHS}mo" positiveSeconds < SEC_TO_YEARS * 20 -> "$sign${positiveSeconds / SEC_TO_YEARS}y" else -> "--" } @@ -50,4 +50,4 @@ class TimeSinceFormatter( private val SEC_TO_MONTHS = 60 * 60 * 24 * 30 private val SEC_TO_YEARS = 60 * 60 * 24 * 365 } -} \ No newline at end of file +} diff --git a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/di/DomainModule.kt b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/di/DomainModule.kt index 7638f64ce..84e3adcc7 100644 --- a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/di/DomainModule.kt +++ b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/di/DomainModule.kt @@ -2,6 +2,7 @@ package uk.co.sentinelweb.cuer.di import org.koin.dsl.module import uk.co.sentinelweb.cuer.core.mappers.DateTimeFormatter +import uk.co.sentinelweb.cuer.core.mappers.FileSizeMapper import uk.co.sentinelweb.cuer.core.mappers.TimeFormatter import uk.co.sentinelweb.cuer.core.mappers.TimeSinceFormatter import uk.co.sentinelweb.cuer.core.providers.CoroutineContextProvider @@ -18,6 +19,7 @@ object DomainModule { factory { TimeProviderImpl() } factory { TimeSinceFormatter(get(), get()) } factory { TimeFormatter() } + factory { FileSizeMapper() } factory { DateTimeFormatter() } } @@ -29,4 +31,4 @@ object DomainModule { } val allModules = listOf(coreModule, domainModule) -} \ No newline at end of file +} diff --git a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/MediaDomain.kt b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/MediaDomain.kt index 0c5d7dbcf..75f9a97ed 100644 --- a/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/MediaDomain.kt +++ b/domain/src/commonMain/kotlin/uk/co/sentinelweb/cuer/domain/MediaDomain.kt @@ -28,6 +28,7 @@ data class MediaDomain( val isLiveBroadcastUpcoming: Boolean = false, val playFromStart: Boolean = false, @Contextual val broadcastDate: LocalDateTime? = null, // todo https://github.com/sentinelweb/cuer/issues/196 + val length: Long? = null, ) : Domain { enum class MediaTypeDomain { diff --git a/domain/src/iosMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/FileSizeMapper.kt b/domain/src/iosMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/FileSizeMapper.kt new file mode 100644 index 000000000..579ffeaaa --- /dev/null +++ b/domain/src/iosMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/FileSizeMapper.kt @@ -0,0 +1,7 @@ +package uk.co.sentinelweb.cuer.core.mappers + +actual class FileSizeMapper { + actual fun formatFileSize(sizeBytes: Long): String { + TODO("Not yet implemented") + } +} diff --git a/domain/src/jsMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/FileSizeMapper.kt b/domain/src/jsMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/FileSizeMapper.kt new file mode 100644 index 000000000..579ffeaaa --- /dev/null +++ b/domain/src/jsMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/FileSizeMapper.kt @@ -0,0 +1,7 @@ +package uk.co.sentinelweb.cuer.core.mappers + +actual class FileSizeMapper { + actual fun formatFileSize(sizeBytes: Long): String { + TODO("Not yet implemented") + } +} diff --git a/domain/src/jvmMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/FileSizeMapper.kt b/domain/src/jvmMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/FileSizeMapper.kt new file mode 100644 index 000000000..bc1b0d49e --- /dev/null +++ b/domain/src/jvmMain/kotlin/uk/co/sentinelweb/cuer/core/mappers/FileSizeMapper.kt @@ -0,0 +1,28 @@ +package uk.co.sentinelweb.cuer.core.mappers + +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +actual class FileSizeMapper { + actual fun formatFileSize(sizeBytes: Long): String = + formatFileSize(bytes = sizeBytes) +} + +/** + * Formats file size in bytes to a human-readable string with appropriate unit (G, M, K). + * @param bytes The file size in bytes + * @param decimalPlaces Number of decimal places to include (default: 1) + * @return Formatted string representation of the file size + */ +fun formatFileSize(bytes: Long, decimalPlaces: Int = 1): String { + if (bytes <= 0) return "0" + + val units = arrayOf("", "K", "M", "G", "T") + val digitGroups = (Math.log10(bytes.toDouble()) / Math.log10(1024.0)).toInt() + + val format = if (decimalPlaces > 0) "%.${decimalPlaces}f %s" else "%.0f %s" + + return String.format( + format, + bytes / Math.pow(1024.0, digitGroups.toDouble()), + units[digitGroups.coerceAtMost(units.size - 1)] + ).trim() +} diff --git a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/filebrowser/FilesUiCoordinator.kt b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/filebrowser/FilesUiCoordinator.kt index f6a90cb85..9a2b938d9 100644 --- a/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/filebrowser/FilesUiCoordinator.kt +++ b/hub/src/main/kotlin/uk/co/sentinelweb/cuer/hub/ui/filebrowser/FilesUiCoordinator.kt @@ -1,9 +1,12 @@ package uk.co.sentinelweb.cuer.hub.ui.filebrowser import androidx.compose.runtime.Composable -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.qualifier.named import org.koin.core.scope.Scope @@ -11,7 +14,7 @@ import org.koin.dsl.module import uk.co.sentinelweb.cuer.app.ui.cast.CastController import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesComposeables.FileBrowserDesktopUi import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesContract -import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesContract.Label.ErrorMessage +import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesContract.Effect.ErrorMessage import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesContract.Model.Companion.Initial import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesContract.State import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesViewModel @@ -52,7 +55,7 @@ class FilesUiCoordinator( private fun bindLabels() { coordinatorScope.launch { - viewModel.labels.collectLatest { + viewModel.effects.collectLatest { when (it) { is ErrorMessage -> parent.showError(it.message) else -> Unit @@ -82,7 +85,7 @@ class FilesUiCoordinator( scope(named()) { scoped { FilesViewModel( - state = State(), + initialState = State(), filesInteractor = get(), remotesRepository = get(), mapper = get(), diff --git a/shared/src/commonMain/composeResources/drawable/ic_episode.xml b/shared/src/commonMain/composeResources/drawable/ic_episode.xml new file mode 100644 index 000000000..a021b4dd0 --- /dev/null +++ b/shared/src/commonMain/composeResources/drawable/ic_episode.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index 5a97e42a8..9fd39b0d3 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -11,6 +11,7 @@ Alphabetical Categories Time + Episode Delete Edit Move diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/common/compose/CuerSharedAppBarComposables.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/common/compose/CuerSharedAppBarComposables.kt index c07729e34..c64f5ea4e 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/common/compose/CuerSharedAppBarComposables.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/common/compose/CuerSharedAppBarComposables.kt @@ -87,7 +87,7 @@ object CuerSharedAppBarComposables { ) { actions.forEach { Action(it, contentColor) } overflow?.also { - TopAppBarOverflowMenu(it, contentColor) + TopAppBarOverflowMenu(it) } } @@ -121,6 +121,7 @@ sealed class CuerMenuItem( object SortAlpha : CuerMenuItem(Res.string.menu_sort_alpha, Res.drawable.ic_sort_by_alpha) object SortCategory : CuerMenuItem(Res.string.menu_sort_category, Res.drawable.ic_category) object SortTime : CuerMenuItem(Res.string.menu_sort_time, Res.drawable.ic_sort_time) + object SortEpisode : CuerMenuItem(Res.string.menu_sort_episode, Res.drawable.ic_episode) object Folders : CuerMenuItem(Res.string.menu_folders, Res.drawable.ic_folder) object ThemeTest : CuerMenuItem(Res.string.menu_theme_test, Res.drawable.ic_edit) @@ -157,6 +158,7 @@ fun TopAppBarOverflowMenu( Icon( painter = painterResource(action.item.icon), contentDescription = null, + tint = contentColor, modifier = Modifier .clickable { expanded = false diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/common/viewmodel/ViewModelEffects.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/common/viewmodel/ViewModelEffects.kt new file mode 100644 index 000000000..b18f83a1b --- /dev/null +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/common/viewmodel/ViewModelEffects.kt @@ -0,0 +1,22 @@ +package uk.co.sentinelweb.cuer.app.ui.common.viewmodel + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch + +class ViewModelEffects { + + private val _effects = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val labels: Flow = _effects + + fun emit(label: Effect, scope: CoroutineScope) = + scope.launch { + _effects.emit(label) + } +} diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/filebrowser/FilesComposeables.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/filebrowser/FilesComposeables.kt index c5721e088..5640032e0 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/filebrowser/FilesComposeables.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/filebrowser/FilesComposeables.kt @@ -2,6 +2,7 @@ package uk.co.sentinelweb.cuer.app.ui.filebrowser import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -23,12 +24,11 @@ import uk.co.sentinelweb.cuer.app.ui.common.compose.CuerSharedAppBarComposables. import uk.co.sentinelweb.cuer.app.ui.common.compose.CuerSharedTheme import uk.co.sentinelweb.cuer.app.ui.common.compose.CustomSnackbar import uk.co.sentinelweb.cuer.app.ui.common.compose.colorTransparentYellow -import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesContract.Label -import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesContract.Label.None +import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesContract.Effect +import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesContract.Effect.None import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesContract.ListItem.ListItemType.* import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesContract.Model.Companion.Initial -import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesContract.Sort.Alpha -import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesContract.Sort.Time +import uk.co.sentinelweb.cuer.app.ui.filebrowser.FilesContract.Sort.* import uk.co.sentinelweb.cuer.domain.Domain import uk.co.sentinelweb.cuer.domain.PlaylistDomain import uk.co.sentinelweb.cuer.domain.PlaylistItemDomain @@ -40,12 +40,12 @@ object FilesComposeables { fun FileBrowserAppUi(viewModel: FilesContract.ViewModel) { val model = viewModel.modelObservable.collectAsState(initial = Initial) val snackbarHostState = remember { SnackbarHostState() } - val label = viewModel.labels.collectAsState(initial = None) + val effects = viewModel.effects.collectAsState(initial = None) - LaunchedEffect(label.value) { - when (label.value) { - is Label.ErrorMessage -> snackbarHostState.showSnackbar( - message = (label.value as Label.ErrorMessage).message, + LaunchedEffect(effects.value) { + when (effects.value) { + is Effect.ErrorMessage -> snackbarHostState.showSnackbar( + message = (effects.value as Effect.ErrorMessage).message, ) else -> Unit @@ -75,13 +75,14 @@ object FilesComposeables { backgroundColor = colorTransparentYellow, onUp = { viewModel.onUpClick() }, actions = listOf( - Action(SortAlpha, { viewModel.onSort(Alpha) }), - Action(SortTime, { viewModel.onSort(Time) }), Action(item = Reload, action = { viewModel.onRefreshClick() }), ), overflowActions = listOf( - Action(Help, { }), + Action(SortAlpha, { viewModel.onSort(Alpha) }), + Action(SortTime, { viewModel.onSort(Time) }), + Action(SortEpisode, { viewModel.onSort(Episode) }), Action(Settings, { viewModel.onSettings() }), + Action(Help, { }), ), modifier = Modifier.fillMaxSize().align(Alignment.CenterStart) ) @@ -145,6 +146,7 @@ object FilesComposeables { @Composable private fun FilesView(model: FilesContract.Model, viewModel: FilesContract.ViewModel) { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + // todo lazy column Column( modifier = Modifier.padding(8.dp) .background(MaterialTheme.colorScheme.surface) @@ -214,26 +216,84 @@ object FilesComposeables { modifier = Modifier.padding(4.dp) ) - if (listItem.timeSince != null) { - Row(Modifier.fillMaxWidth()) { - val text = "${listItem.season ?: ""} ${listItem.ext ?: ""}".trim() - if (text.isNotBlank()) { - Text( - text = text, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(4.dp).align(Alignment.CenterVertically) - ) - } + Row( + Modifier.fillMaxWidth() + .horizontalScroll(rememberScrollState()) + ) { + listItem.episodeNumber?.apply { + Text( + text = this, + style = MaterialTheme.typography.bodyMedium, + color = Color.White, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(4.dp) + .background( + color = Color.Red.darken(0.8f).copy(alpha = 0.5f), + shape = RoundedCornerShape(4.dp) + ) + .padding(4.dp) + .align(CenterVertically) + ) + } + + listItem.ext?.apply { Text( - text = listItem.timeSince, + text = this, + style = MaterialTheme.typography.bodyMedium, + color = Color.White, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(4.dp) + .background( + color = Color.Black.darken(0.9f).copy(alpha = 0.5f), + shape = RoundedCornerShape(4.dp) + ) + .padding(4.dp) + .align(CenterVertically) + ) + } + listItem.sizeText?.apply { + Text( + text = this, + style = MaterialTheme.typography.bodyMedium, + color = Color.White, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(4.dp) + .background( + color = Color.Green.darken(0.5f).copy(alpha = 0.5f), + shape = RoundedCornerShape(4.dp) + ) + .padding(4.dp) + .align(CenterVertically) + ) + } + listItem.timeSince?.apply { + Text( + text = this, style = MaterialTheme.typography.bodySmall, - color = Color.Gray, - modifier = Modifier.padding(2.dp).align(Alignment.CenterVertically) + color = Color.White, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(4.dp) + .background( + color = Color.Blue.darken(0.5f).copy(alpha = 0.5f), + shape = RoundedCornerShape(4.dp) + ).padding(4.dp) + .align(CenterVertically) ) } } } } } + + private fun Color.darken(factor: Float) = let { + it.copy( + red = it.red * factor, + green = it.green * factor, + blue = it.blue * factor + ) + } } diff --git a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/filebrowser/FilesContract.kt b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/filebrowser/FilesContract.kt index 7df8c42de..df3e88729 100644 --- a/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/filebrowser/FilesContract.kt +++ b/shared/src/commonMain/kotlin/uk/co/sentinelweb/cuer/app/ui/filebrowser/FilesContract.kt @@ -11,7 +11,7 @@ interface FilesContract { interface ViewModel { val modelObservable: Flow - val labels: Flow