diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c1e5bcebeb..ded24501d1 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -51,7 +51,6 @@ jobs:
./gradlew spotlessCheck \
:android-app:app:bundle \
:android-app:app:build \
- jvmTest \
lint \
-x :android-app:app:assembleStandardBenchmark \
-x :android-app:app:bundleStandardBenchmark
@@ -102,7 +101,7 @@ jobs:
path: |
**/build/test-results/*
- ios:
+ desktop:
runs-on: macos-latest
timeout-minutes: 60
env:
@@ -110,9 +109,6 @@ jobs:
ORG_GRADLE_PROJECT_TIVI_TVDB_API_KEY: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_TVDB_API_KEY }}
ORG_GRADLE_PROJECT_TIVI_TRAKT_CLIENT_ID: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_TRAKT_CLIENT_ID }}
ORG_GRADLE_PROJECT_TIVI_TRAKT_CLIENT_SECRET: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_TRAKT_CLIENT_SECRET }}
- ORG_GRADLE_PROJECT_TIVI_RELEASE_KEYSTORE_PWD: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_RELEASE_KEYSTORE_PWD }}
- ORG_GRADLE_PROJECT_TIVI_RELEASE_KEY_PWD: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_RELEASE_KEY_PWD }}
- ORG_GRADLE_PROJECT_TIVI_PLAY_PUBLISHER_ACCOUNT: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_PLAY_PUBLISHER_ACCOUNT }}
steps:
- uses: actions/checkout@v3
@@ -130,17 +126,59 @@ jobs:
with:
gradle-home-cache-cleanup: true
- - name: Decrypt secrets
- run: ./release/decrypt-secrets.sh
- env:
- ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
+ - name: Build Desktop App
+ run: ./gradlew spotlessCheck jvmTest :desktop-app:package
- - name: Build iOS libraries
- run: ./gradlew spotlessCheck linkIosX64 iosX64Test
+ - name: Upload build outputs
+ if: always()
+ uses: actions/upload-artifact@v3
+ with:
+ name: desktop-build-binaries
+ path: desktop-app/build/compose/binaries
- - name: Clean secrets
+ - name: Upload reports
if: always()
- run: ./release/clean-secrets.sh
+ uses: actions/upload-artifact@v3
+ with:
+ name: desktop-reports
+ path: |
+ **/build/reports/*
+
+ - name: Upload test results
+ if: always()
+ uses: actions/upload-artifact@v3
+ with:
+ name: desktop-test-results
+ path: |
+ **/build/test-results/*
+
+ ios:
+ runs-on: macos-latest
+ timeout-minutes: 60
+ env:
+ ORG_GRADLE_PROJECT_TIVI_TMDB_API_KEY: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_TMDB_API_KEY }}
+ ORG_GRADLE_PROJECT_TIVI_TVDB_API_KEY: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_TVDB_API_KEY }}
+ ORG_GRADLE_PROJECT_TIVI_TRAKT_CLIENT_ID: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_TRAKT_CLIENT_ID }}
+ ORG_GRADLE_PROJECT_TIVI_TRAKT_CLIENT_SECRET: ${{ secrets.ORG_GRADLE_PROJECT_TIVI_TRAKT_CLIENT_SECRET }}
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Validate Gradle Wrapper
+ uses: gradle/wrapper-validation-action@v1
+
+ - name: set up JDK
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'zulu'
+ java-version: 17
+
+ - uses: gradle/gradle-build-action@v2
+ with:
+ gradle-home-cache-cleanup: true
+
+ - name: Build iOS libraries
+ run: ./gradlew spotlessCheck :shared:linkIosX64 iosX64Test
- name: Upload reports
if: always()
@@ -160,7 +198,7 @@ jobs:
publish:
if: github.ref == 'refs/heads/main'
- needs: [android, ios]
+ needs: [android, ios, desktop]
runs-on: ubuntu-latest
timeout-minutes: 20
env:
diff --git a/.gitignore b/.gitignore
index 81ec623178..fb35aea523 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,99 @@ org.eclipse.buildship.core.prefs
.classpath
.project
bin/
+
+
+##########################################################################################
+# Imported from https://github.com/github/gitignore/blob/main/Swift.gitignore
+##########################################################################################
+
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## User settings
+xcuserdata/
+
+## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
+*.xcscmblueprint
+*.xccheckout
+
+## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
+build/
+DerivedData/
+*.moved-aside
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+
+## Obj-C/Swift specific
+*.hmap
+
+## App packaging
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+# Package.pins
+# Package.resolved
+# *.xcodeproj
+#
+# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
+# hence it is not needed unless you have added a package configuration file to your project
+# .swiftpm
+
+.build/
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# Pods/
+#
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build/
+
+# Accio dependency management
+Dependencies/
+.accio/
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo.
+# Instead, use fastlane to re-generate the screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots/**/*.png
+fastlane/test_output
+
+# Code Injection
+#
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+iOSInjectionProject/
diff --git a/.idea/dictionaries/chris.xml b/.idea/dictionaries/chris.xml
new file mode 100644
index 0000000000..06d5fdf03b
--- /dev/null
+++ b/.idea/dictionaries/chris.xml
@@ -0,0 +1,7 @@
+
+
+
+ spdx
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 9fee720a8d..69e86158ba 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,10 +1,6 @@
-
-
-
-
-
+
\ No newline at end of file
diff --git a/android-app/app/build.gradle.kts b/android-app/app/build.gradle.kts
index 92e4f0b3a5..168cd48cd2 100644
--- a/android-app/app/build.gradle.kts
+++ b/android-app/app/build.gradle.kts
@@ -4,9 +4,9 @@
plugins {
id("app.tivi.android.application")
- id("app.tivi.android.compose")
id("app.tivi.kotlin.android")
alias(libs.plugins.ksp)
+ alias(libs.plugins.composeMultiplatform)
}
val appVersionCode = propOrDef("TIVI_VERSIONCODE", "1000").toInt()
@@ -148,51 +148,22 @@ androidComponents {
dependencies {
implementation(projects.shared)
-
- implementation(projects.ui.account)
- implementation(projects.ui.discover)
- implementation(projects.ui.episode.details)
- implementation(projects.ui.episode.track)
- implementation(projects.ui.library)
- implementation(projects.ui.popular)
- implementation(projects.ui.trending)
- implementation(projects.ui.recommended)
- implementation(projects.ui.search)
- implementation(projects.ui.show.details)
- implementation(projects.ui.show.seasons)
implementation(projects.ui.settings)
- implementation(projects.ui.upnext)
-
- implementation(libs.circuit.overlay)
-
- implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.activity.activity)
implementation(libs.androidx.activity.compose)
-
implementation(libs.androidx.emoji)
-
- implementation(libs.compose.foundation.foundation)
- implementation(libs.compose.foundation.layout)
- implementation(libs.compose.material.material)
- implementation(libs.compose.material.iconsext)
- implementation(libs.compose.material3.material3)
- implementation(libs.compose.material3.windowsizeclass)
- implementation(libs.compose.animation.animation)
- implementation(libs.compose.ui.tooling)
-
- implementation(libs.timber)
+ implementation(libs.androidx.lifecycle.viewmodel.ktx)
+ implementation(libs.androidx.profileinstaller)
implementation(libs.kotlin.coroutines.android)
- implementation(libs.androidx.profileinstaller)
+ implementation(libs.google.firebase.crashlytics)
implementation(libs.okhttp.loggingInterceptor)
ksp(libs.kotlininject.compiler)
- implementation(libs.google.firebase.crashlytics)
-
qaImplementation(libs.chucker.library)
qaImplementation(libs.debugdrawer.debugdrawer)
diff --git a/android-app/app/src/main/java/app/tivi/home/MainActivity.kt b/android-app/app/src/main/java/app/tivi/home/MainActivity.kt
deleted file mode 100644
index 6436cac535..0000000000
--- a/android-app/app/src/main/java/app/tivi/home/MainActivity.kt
+++ /dev/null
@@ -1,191 +0,0 @@
-// Copyright 2017, Google LLC, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package app.tivi.home
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.viewModels
-import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
-import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.ComposeView
-import androidx.core.view.WindowCompat
-import androidx.lifecycle.findViewTreeLifecycleOwner
-import androidx.lifecycle.findViewTreeViewModelStoreOwner
-import androidx.lifecycle.setViewTreeLifecycleOwner
-import androidx.lifecycle.setViewTreeViewModelStoreOwner
-import androidx.lifecycle.viewmodel.viewModelFactory
-import androidx.savedstate.findViewTreeSavedStateRegistryOwner
-import androidx.savedstate.setViewTreeSavedStateRegistryOwner
-import app.tivi.ContentViewSetter
-import app.tivi.TiviActivity
-import app.tivi.common.compose.LocalTiviDateFormatter
-import app.tivi.common.compose.LocalTiviTextCreator
-import app.tivi.common.compose.LocalWindowSizeClass
-import app.tivi.common.compose.shouldUseDarkColors
-import app.tivi.common.compose.shouldUseDynamicColors
-import app.tivi.common.compose.theme.TiviTheme
-import app.tivi.core.analytics.Analytics
-import app.tivi.data.traktauth.LoginToTraktInteractor
-import app.tivi.data.traktauth.TraktAuthActivityComponent
-import app.tivi.extensions.unsafeLazy
-import app.tivi.inject.ActivityComponent
-import app.tivi.inject.ActivityScope
-import app.tivi.inject.AndroidApplicationComponent
-import app.tivi.overlays.LocalNavigator
-import app.tivi.screens.DiscoverScreen
-import app.tivi.screens.SettingsScreen
-import app.tivi.screens.TiviScreen
-import app.tivi.settings.SettingsActivity
-import app.tivi.settings.TiviPreferences
-import app.tivi.util.TiviDateFormatter
-import app.tivi.util.TiviTextCreator
-import com.slack.circuit.backstack.SaveableBackStack
-import com.slack.circuit.backstack.rememberSaveableBackStack
-import com.slack.circuit.foundation.CircuitCompositionLocals
-import com.slack.circuit.foundation.CircuitConfig
-import com.slack.circuit.foundation.push
-import com.slack.circuit.foundation.rememberCircuitNavigator
-import com.slack.circuit.foundation.screen
-import com.slack.circuit.runtime.Navigator
-import com.slack.circuit.runtime.Screen
-import me.tatarka.inject.annotations.Component
-import me.tatarka.inject.annotations.Provides
-
-class MainActivity : TiviActivity() {
-
- private lateinit var component: MainActivityComponent
-
- private val viewModel: MainActivityViewModel by viewModels {
- viewModelFactory {
- addInitializer(MainActivityViewModel::class) { component.viewModel() }
- }
- }
-
- private val preferences: TiviPreferences by unsafeLazy { component.preferences }
- private val analytics: Analytics by unsafeLazy { component.analytics }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- component = MainActivityComponent::class.create(this)
-
- WindowCompat.setDecorFitsSystemWindows(window, false)
-
- // Get the viewModel, so it is started and 'running'
- viewModel
-
- val composeView = ComposeView(this).apply {
- setContent {
- TiviContent()
- }
- }
-
- // Copied from setContent {} ext-fun
- setOwners()
-
- // Register for Login activity results
- component.login.register()
-
- component.contentViewSetter.setContentView(this, composeView)
- }
-
- @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
- @Composable
- private fun TiviContent() {
- CircuitCompositionLocals(component.circuitConfig) {
- val backstack: SaveableBackStack = rememberSaveableBackStack { push(DiscoverScreen) }
- val circuitNavigator = rememberCircuitNavigator(backstack)
-
- val navigator: Navigator = remember(circuitNavigator) {
- TiviNavigator(context = this, navigator = circuitNavigator)
- }
-
- // Launch an effect to track changes to the current back stack entry, and push them
- // as a screen views to analytics
- LaunchedEffect(backstack.topRecord) {
- val topScreen = backstack.topRecord?.screen as? TiviScreen
- analytics.trackScreenView(
- name = topScreen?.name ?: "unknown screen",
- arguments = topScreen?.arguments,
- )
- }
-
- CompositionLocalProvider(
- LocalTiviDateFormatter provides component.tiviDateFormatter,
- LocalTiviTextCreator provides component.textCreator,
- LocalNavigator provides navigator,
- LocalWindowSizeClass provides calculateWindowSizeClass(this@MainActivity),
- ) {
- TiviTheme(
- useDarkColors = preferences.shouldUseDarkColors(),
- useDynamicColors = preferences.shouldUseDynamicColors(),
- ) {
- Home(
- backstack = backstack,
- navigator = navigator,
- )
- }
- }
- }
- }
-}
-
-internal class TiviNavigator(
- private val context: Context,
- private val navigator: Navigator,
-) : Navigator {
- override fun goTo(screen: Screen) {
- when (screen) {
- is SettingsScreen -> {
- // We need to 'escape' out of Compose here and launch an activity
- context.startActivity(Intent(context, SettingsActivity::class.java))
- }
- else -> navigator.goTo(screen)
- }
- }
-
- override fun pop(): Screen? {
- return navigator.pop()
- }
-
- override fun resetRoot(newRoot: Screen): List {
- return navigator.resetRoot(newRoot)
- }
-}
-
-@ActivityScope
-@Component
-abstract class MainActivityComponent(
- @get:Provides override val activity: Activity,
- @Component val applicationComponent: AndroidApplicationComponent = AndroidApplicationComponent.from(activity),
-) : ActivityComponent,
- TraktAuthActivityComponent {
- abstract val tiviDateFormatter: TiviDateFormatter
- abstract val textCreator: TiviTextCreator
- abstract val preferences: TiviPreferences
- abstract val analytics: Analytics
- abstract val contentViewSetter: ContentViewSetter
- abstract val login: LoginToTraktInteractor
- abstract val circuitConfig: CircuitConfig
- abstract val viewModel: () -> MainActivityViewModel
-}
-
-private fun ComponentActivity.setOwners() {
- val decorView = window.decorView
- if (decorView.findViewTreeLifecycleOwner() == null) {
- decorView.setViewTreeLifecycleOwner(this)
- }
- if (decorView.findViewTreeViewModelStoreOwner() == null) {
- decorView.setViewTreeViewModelStoreOwner(this)
- }
- if (decorView.findViewTreeSavedStateRegistryOwner() == null) {
- decorView.setViewTreeSavedStateRegistryOwner(this)
- }
-}
diff --git a/android-app/app/src/main/java/app/tivi/ContentViewSetter.kt b/android-app/app/src/main/kotlin/app/tivi/ContentViewSetter.kt
similarity index 100%
rename from android-app/app/src/main/java/app/tivi/ContentViewSetter.kt
rename to android-app/app/src/main/kotlin/app/tivi/ContentViewSetter.kt
diff --git a/android-app/app/src/main/java/app/tivi/TiviActivity.kt b/android-app/app/src/main/kotlin/app/tivi/TiviActivity.kt
similarity index 100%
rename from android-app/app/src/main/java/app/tivi/TiviActivity.kt
rename to android-app/app/src/main/kotlin/app/tivi/TiviActivity.kt
diff --git a/android-app/app/src/main/java/app/tivi/TiviApplication.kt b/android-app/app/src/main/kotlin/app/tivi/TiviApplication.kt
similarity index 95%
rename from android-app/app/src/main/java/app/tivi/TiviApplication.kt
rename to android-app/app/src/main/kotlin/app/tivi/TiviApplication.kt
index ecb8172a32..ce61d9f9ac 100644
--- a/android-app/app/src/main/java/app/tivi/TiviApplication.kt
+++ b/android-app/app/src/main/kotlin/app/tivi/TiviApplication.kt
@@ -22,7 +22,7 @@ class TiviApplication : Application(), Configuration.Provider {
workerFactory = component.workerFactory
- component.initializers.init()
+ component.initializers.initialize()
}
override fun getWorkManagerConfiguration(): Configuration {
diff --git a/android-app/app/src/main/java/app/tivi/appinitializers/EmojiInitializer.kt b/android-app/app/src/main/kotlin/app/tivi/appinitializers/EmojiInitializer.kt
similarity index 96%
rename from android-app/app/src/main/java/app/tivi/appinitializers/EmojiInitializer.kt
rename to android-app/app/src/main/kotlin/app/tivi/appinitializers/EmojiInitializer.kt
index 03acf193ca..e60851f25f 100644
--- a/android-app/app/src/main/java/app/tivi/appinitializers/EmojiInitializer.kt
+++ b/android-app/app/src/main/kotlin/app/tivi/appinitializers/EmojiInitializer.kt
@@ -13,7 +13,7 @@ import me.tatarka.inject.annotations.Inject
class EmojiInitializer(
private val application: Application,
) : AppInitializer {
- override fun init() {
+ override fun initialize() {
val fontRequest = FontRequest(
"com.google.android.gms.fonts",
"com.google.android.gms",
diff --git a/android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt b/android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt
new file mode 100644
index 0000000000..df1f6011d9
--- /dev/null
+++ b/android-app/app/src/main/kotlin/app/tivi/home/MainActivity.kt
@@ -0,0 +1,110 @@
+// Copyright 2017, Google LLC, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.home
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.viewModels
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
+import androidx.core.view.WindowCompat
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
+import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.lifecycle.setViewTreeViewModelStoreOwner
+import androidx.lifecycle.viewmodel.viewModelFactory
+import androidx.savedstate.findViewTreeSavedStateRegistryOwner
+import androidx.savedstate.setViewTreeSavedStateRegistryOwner
+import app.tivi.ContentViewSetter
+import app.tivi.TiviActivity
+import app.tivi.data.traktauth.LoginToTraktInteractor
+import app.tivi.data.traktauth.TraktAuthActivityComponent
+import app.tivi.inject.ActivityComponent
+import app.tivi.inject.ActivityScope
+import app.tivi.inject.AndroidApplicationComponent
+import app.tivi.inject.UiComponent
+import app.tivi.settings.SettingsActivity
+import me.tatarka.inject.annotations.Component
+import me.tatarka.inject.annotations.Provides
+
+class MainActivity : TiviActivity() {
+
+ private lateinit var component: MainActivityComponent
+
+ private val viewModel: MainActivityViewModel by viewModels {
+ viewModelFactory {
+ addInitializer(MainActivityViewModel::class) { component.viewModel() }
+ }
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ component = MainActivityComponent::class.create(this)
+
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ // Get the viewModel, so it is started and 'running'
+ viewModel
+
+ val composeView = ComposeView(this).apply {
+ setContent {
+ component.tiviContent(
+ onRootPop = {
+ if (onBackPressedDispatcher.hasEnabledCallbacks()) {
+ onBackPressedDispatcher.onBackPressed()
+ }
+ },
+ onOpenSettings = {
+ context.startActivity(Intent(context, SettingsActivity::class.java))
+ },
+ modifier = Modifier.semantics {
+ // Enables testTag -> UiAutomator resource id
+ // See https://developer.android.com/jetpack/compose/testing#uiautomator-interop
+ testTagsAsResourceId = true
+ },
+ )
+ }
+ }
+
+ // Copied from setContent {} ext-fun
+ setOwners()
+
+ // Register for Login activity results
+ component.login.register()
+
+ component.contentViewSetter.setContentView(this, composeView)
+ }
+}
+
+@ActivityScope
+@Component
+abstract class MainActivityComponent(
+ @get:Provides override val activity: Activity,
+ @Component val applicationComponent: AndroidApplicationComponent = AndroidApplicationComponent.from(activity),
+) : ActivityComponent, TraktAuthActivityComponent, UiComponent {
+ abstract val tiviContent: TiviContent
+
+ abstract val contentViewSetter: ContentViewSetter
+ abstract val login: LoginToTraktInteractor
+ abstract val viewModel: () -> MainActivityViewModel
+}
+
+private fun ComponentActivity.setOwners() {
+ val decorView = window.decorView
+ if (decorView.findViewTreeLifecycleOwner() == null) {
+ decorView.setViewTreeLifecycleOwner(this)
+ }
+ if (decorView.findViewTreeViewModelStoreOwner() == null) {
+ decorView.setViewTreeViewModelStoreOwner(this)
+ }
+ if (decorView.findViewTreeSavedStateRegistryOwner() == null) {
+ decorView.setViewTreeSavedStateRegistryOwner(this)
+ }
+}
diff --git a/android-app/app/src/main/java/app/tivi/home/MainActivityViewModel.kt b/android-app/app/src/main/kotlin/app/tivi/home/MainActivityViewModel.kt
similarity index 100%
rename from android-app/app/src/main/java/app/tivi/home/MainActivityViewModel.kt
rename to android-app/app/src/main/kotlin/app/tivi/home/MainActivityViewModel.kt
diff --git a/android-app/app/src/main/java/app/tivi/inject/ActivityComponent.kt b/android-app/app/src/main/kotlin/app/tivi/inject/ActivityComponent.kt
similarity index 100%
rename from android-app/app/src/main/java/app/tivi/inject/ActivityComponent.kt
rename to android-app/app/src/main/kotlin/app/tivi/inject/ActivityComponent.kt
diff --git a/android-app/app/src/main/java/app/tivi/inject/AndroidApplicationComponent.kt b/android-app/app/src/main/kotlin/app/tivi/inject/AndroidApplicationComponent.kt
similarity index 95%
rename from android-app/app/src/main/java/app/tivi/inject/AndroidApplicationComponent.kt
rename to android-app/app/src/main/kotlin/app/tivi/inject/AndroidApplicationComponent.kt
index adef63802e..6568f9b47f 100644
--- a/android-app/app/src/main/java/app/tivi/inject/AndroidApplicationComponent.kt
+++ b/android-app/app/src/main/kotlin/app/tivi/inject/AndroidApplicationComponent.kt
@@ -5,6 +5,7 @@ package app.tivi.inject
import android.app.Application
import android.content.Context
+import androidx.compose.ui.unit.Density
import app.tivi.BuildConfig
import app.tivi.TiviApplication
import app.tivi.app.ApplicationInfo
@@ -12,7 +13,6 @@ import app.tivi.app.Flavor
import app.tivi.appinitializers.AppInitializer
import app.tivi.appinitializers.AppInitializers
import app.tivi.appinitializers.EmojiInitializer
-import app.tivi.common.imageloading.ImageLoadingComponent
import app.tivi.home.ContentViewSetterComponent
import app.tivi.tasks.TiviWorkerFactory
import java.io.File
@@ -31,8 +31,6 @@ import okhttp3.OkHttpClient
abstract class AndroidApplicationComponent(
@get:Provides val application: Application,
) : SharedApplicationComponent,
- UiComponent,
- ImageLoadingComponent,
VariantAwareComponent,
ContentViewSetterComponent {
@@ -78,6 +76,9 @@ abstract class AndroidApplicationComponent(
.writeTimeout(20, TimeUnit.SECONDS)
.build()
+ @Provides
+ fun provideDensity(application: Application): Density = Density(application)
+
companion object {
fun from(context: Context): AndroidApplicationComponent {
return (context.applicationContext as TiviApplication).component
diff --git a/android-app/benchmark/src/main/java/app/tivi/benchmark/BaselineProfileGenerator.kt b/android-app/benchmark/src/main/kotlin/app/tivi/benchmark/BaselineProfileGenerator.kt
similarity index 100%
rename from android-app/benchmark/src/main/java/app/tivi/benchmark/BaselineProfileGenerator.kt
rename to android-app/benchmark/src/main/kotlin/app/tivi/benchmark/BaselineProfileGenerator.kt
diff --git a/android-app/benchmark/src/main/java/app/tivi/benchmark/StartupBenchmark.kt b/android-app/benchmark/src/main/kotlin/app/tivi/benchmark/StartupBenchmark.kt
similarity index 100%
rename from android-app/benchmark/src/main/java/app/tivi/benchmark/StartupBenchmark.kt
rename to android-app/benchmark/src/main/kotlin/app/tivi/benchmark/StartupBenchmark.kt
diff --git a/android-app/common-test/src/main/java/app/tivi/app/test/AppScenarios.kt b/android-app/common-test/src/main/kotlin/app/tivi/app/test/AppScenarios.kt
similarity index 100%
rename from android-app/common-test/src/main/java/app/tivi/app/test/AppScenarios.kt
rename to android-app/common-test/src/main/kotlin/app/tivi/app/test/AppScenarios.kt
diff --git a/api/tmdb/build.gradle.kts b/api/tmdb/build.gradle.kts
index 8ea76a1e8c..deb4b7dc6d 100644
--- a/api/tmdb/build.gradle.kts
+++ b/api/tmdb/build.gradle.kts
@@ -31,7 +31,7 @@ kotlin {
val jvmMain by getting {
dependencies {
- implementation(libs.okhttp.okhttp)
+ api(libs.okhttp.okhttp)
implementation(libs.ktor.client.okhttp)
}
}
diff --git a/api/trakt/build.gradle.kts b/api/trakt/build.gradle.kts
index 9b08835f70..8b732bbf57 100644
--- a/api/trakt/build.gradle.kts
+++ b/api/trakt/build.gradle.kts
@@ -36,7 +36,7 @@ kotlin {
val jvmMain by getting {
dependencies {
- implementation(libs.okhttp.okhttp)
+ api(libs.okhttp.okhttp)
implementation(libs.ktor.client.okhttp)
}
}
diff --git a/build.gradle.kts b/build.gradle.kts
index 0a31029ff0..0794aec806 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -19,10 +19,10 @@ plugins {
alias(libs.plugins.gms.googleServices) apply false
alias(libs.plugins.firebase.crashlytics) apply false
alias(libs.plugins.spotless) apply false
+ alias(libs.plugins.composeMultiplatform) apply false
}
allprojects {
-
// Workaround for https://issuetracker.google.com/issues/268961156
tasks.withType {
tasks.findByName("kspTestKotlin")?.let {
diff --git a/common/imageloading/build.gradle.kts b/common/imageloading/build.gradle.kts
index e6ee99f71c..90e61e566e 100644
--- a/common/imageloading/build.gradle.kts
+++ b/common/imageloading/build.gradle.kts
@@ -4,27 +4,32 @@
plugins {
id("app.tivi.android.library")
- id("app.tivi.kotlin.android")
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
}
-android {
- namespace = "app.tivi.common.imageloading"
-}
-
-dependencies {
- implementation(projects.core.base)
- implementation(projects.core.logging)
- implementation(projects.core.powercontroller)
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(projects.core.base)
+ implementation(projects.core.logging)
+ implementation(projects.core.powercontroller)
- implementation(projects.data.models)
- implementation(projects.data.episodes)
- implementation(projects.data.showimages)
+ implementation(projects.data.models)
+ implementation(projects.data.episodes)
+ implementation(projects.data.showimages)
- implementation(projects.api.tmdb)
+ implementation(projects.api.tmdb)
- implementation(libs.androidx.core)
+ implementation(libs.kotlininject.runtime)
- implementation(libs.kotlininject.runtime)
+ api(libs.imageloader)
+ }
+ }
+ }
+}
- api(libs.coil.coil)
+android {
+ namespace = "app.tivi.common.imageloading"
}
diff --git a/common/imageloading/src/androidMain/kotlin/app/tivi/common/imageloading/AndroidImageLoaderFactory.kt b/common/imageloading/src/androidMain/kotlin/app/tivi/common/imageloading/AndroidImageLoaderFactory.kt
new file mode 100644
index 0000000000..87e59d4c48
--- /dev/null
+++ b/common/imageloading/src/androidMain/kotlin/app/tivi/common/imageloading/AndroidImageLoaderFactory.kt
@@ -0,0 +1,39 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.imageloading
+
+import android.content.Context
+import com.seiko.imageloader.ImageLoader
+import com.seiko.imageloader.ImageLoaderConfigBuilder
+import com.seiko.imageloader.cache.memory.maxSizePercent
+import com.seiko.imageloader.component.setupDefaultComponents
+import com.seiko.imageloader.option.androidContext
+import okio.Path.Companion.toOkioPath
+
+internal class AndroidImageLoaderFactory(
+ private val context: Context,
+) : ImageLoaderFactory {
+ override fun create(
+ block: ImageLoaderConfigBuilder.() -> Unit,
+ ): ImageLoader = ImageLoader {
+ options {
+ androidContext(context.applicationContext)
+ }
+ components {
+ setupDefaultComponents()
+ }
+ interceptor {
+ memoryCacheConfig {
+ // Set the max size to 25% of the app's available memory.
+ maxSizePercent(context.applicationContext, 0.25)
+ }
+ diskCacheConfig {
+ directory(context.cacheDir.resolve("image_cache").toOkioPath())
+ maxSizeBytes(512L * 1024 * 1024) // 512MB
+ }
+ }
+
+ block()
+ }
+}
diff --git a/common/imageloading/src/androidMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt b/common/imageloading/src/androidMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt
new file mode 100644
index 0000000000..aee64378bd
--- /dev/null
+++ b/common/imageloading/src/androidMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt
@@ -0,0 +1,25 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.imageloading
+
+import android.app.Application
+import app.tivi.util.Logger
+import com.seiko.imageloader.ImageLoader
+import com.seiko.imageloader.intercept.Interceptor
+import me.tatarka.inject.annotations.Provides
+
+actual interface ImageLoadingPlatformComponent {
+ @Provides
+ fun provideImageLoader(
+ application: Application,
+ interceptors: Set,
+ logger: Logger,
+ ): ImageLoader = AndroidImageLoaderFactory(application).create {
+ this.logger = logger.asImageLoaderLogger()
+
+ interceptor {
+ addInterceptors(interceptors)
+ }
+ }
+}
diff --git a/common/imageloading/src/main/java/app/tivi/common/imageloading/EpisodeCoilInterceptor.kt b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/EpisodeCoilInterceptor.kt
similarity index 67%
rename from common/imageloading/src/main/java/app/tivi/common/imageloading/EpisodeCoilInterceptor.kt
rename to common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/EpisodeCoilInterceptor.kt
index f4e8b78ffa..5e583878ac 100644
--- a/common/imageloading/src/main/java/app/tivi/common/imageloading/EpisodeCoilInterceptor.kt
+++ b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/EpisodeCoilInterceptor.kt
@@ -3,24 +3,23 @@
package app.tivi.common.imageloading
+import androidx.compose.ui.unit.Density
import app.tivi.data.episodes.SeasonsEpisodesRepository
import app.tivi.data.imagemodels.EpisodeImageModel
import app.tivi.data.util.inPast
import app.tivi.tmdb.TmdbImageUrlProvider
-import coil.intercept.Interceptor
-import coil.request.ImageRequest
-import coil.request.ImageResult
-import coil.size.Size
-import coil.size.pxOrElse
+import com.seiko.imageloader.intercept.Interceptor
+import com.seiko.imageloader.model.ImageRequest
+import com.seiko.imageloader.model.ImageResult
+import kotlin.math.roundToInt
import kotlin.time.Duration.Companion.days
import me.tatarka.inject.annotations.Inject
-import okhttp3.HttpUrl
-import okhttp3.HttpUrl.Companion.toHttpUrl
@Inject
class EpisodeCoilInterceptor(
private val tmdbImageUrlProvider: Lazy,
private val repository: SeasonsEpisodesRepository,
+ private val density: () -> Density,
) : Interceptor {
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val request = when (val data = chain.request.data) {
@@ -36,16 +35,16 @@ class EpisodeCoilInterceptor(
}
return repository.getEpisode(model.id)?.tmdbBackdropPath?.let { backdropPath ->
- chain.request.newBuilder()
- .data(map(backdropPath, chain.size))
- .build()
- } ?: chain.request
- }
+ val size = chain.options.sizeResolver.run { density().size() }
- private fun map(backdropPath: String, size: Size): HttpUrl {
- return tmdbImageUrlProvider.value.getBackdropUrl(
- path = backdropPath,
- imageWidth = size.width.pxOrElse { 0 },
- ).toHttpUrl()
+ chain.request.newBuilder {
+ data(
+ tmdbImageUrlProvider.value.getBackdropUrl(
+ path = backdropPath,
+ imageWidth = size.width.roundToInt(),
+ ),
+ )
+ }
+ } ?: chain.request
}
}
diff --git a/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ImageLoaderFactory.kt b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ImageLoaderFactory.kt
new file mode 100644
index 0000000000..b69ce21df6
--- /dev/null
+++ b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ImageLoaderFactory.kt
@@ -0,0 +1,38 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.imageloading
+
+import app.tivi.util.Logger
+import com.seiko.imageloader.ImageLoader
+import com.seiko.imageloader.ImageLoaderConfigBuilder
+import com.seiko.imageloader.util.LogPriority
+
+internal fun interface ImageLoaderFactory {
+ fun create(
+ block: ImageLoaderConfigBuilder.() -> Unit,
+ ): ImageLoader
+}
+
+internal fun Logger.asImageLoaderLogger(): com.seiko.imageloader.util.Logger {
+ return object : com.seiko.imageloader.util.Logger {
+ override fun isLoggable(priority: LogPriority): Boolean = true
+
+ override fun log(
+ priority: LogPriority,
+ tag: String,
+ data: Any?,
+ throwable: Throwable?,
+ message: String,
+ ) {
+ when (priority) {
+ LogPriority.VERBOSE -> v(throwable) { message }
+ LogPriority.DEBUG -> d(throwable) { message }
+ LogPriority.INFO -> i(throwable) { message }
+ LogPriority.WARN -> i(throwable) { message }
+ LogPriority.ERROR -> e(throwable) { message }
+ LogPriority.ASSERT -> e(throwable) { message }
+ }
+ }
+ }
+}
diff --git a/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ImageLoadingComponent.kt b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ImageLoadingComponent.kt
new file mode 100644
index 0000000000..351939245b
--- /dev/null
+++ b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ImageLoadingComponent.kt
@@ -0,0 +1,28 @@
+// Copyright 2019, Google LLC, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.imageloading
+
+import com.seiko.imageloader.ImageLoader
+import com.seiko.imageloader.intercept.Interceptor
+import me.tatarka.inject.annotations.IntoSet
+import me.tatarka.inject.annotations.Provides
+
+expect interface ImageLoadingPlatformComponent
+
+interface ImageLoadingComponent : ImageLoadingPlatformComponent {
+
+ val imageLoader: ImageLoader
+
+ @Provides
+ @IntoSet
+ fun provideShowCoilInterceptor(interceptor: ShowCoilInterceptor): Interceptor = interceptor
+
+ @Provides
+ @IntoSet
+ fun provideTmdbImageEntityCoilInterceptor(interceptor: TmdbImageEntityCoilInterceptor): Interceptor = interceptor
+
+ @Provides
+ @IntoSet
+ fun provideEpisodeCoilInterceptor(interceptor: EpisodeCoilInterceptor): Interceptor = interceptor
+}
diff --git a/common/imageloading/src/main/java/app/tivi/common/imageloading/ShowCoilInterceptor.kt b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ShowCoilInterceptor.kt
similarity index 77%
rename from common/imageloading/src/main/java/app/tivi/common/imageloading/ShowCoilInterceptor.kt
rename to common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ShowCoilInterceptor.kt
index 5e6f1a47ae..1a3c0ded2c 100644
--- a/common/imageloading/src/main/java/app/tivi/common/imageloading/ShowCoilInterceptor.kt
+++ b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/ShowCoilInterceptor.kt
@@ -3,6 +3,7 @@
package app.tivi.common.imageloading
+import androidx.compose.ui.unit.Density
import app.tivi.data.imagemodels.ShowImageModel
import app.tivi.data.models.ImageType
import app.tivi.data.models.TmdbImageEntity
@@ -10,10 +11,10 @@ import app.tivi.data.showimages.ShowImagesStore
import app.tivi.tmdb.TmdbImageUrlProvider
import app.tivi.util.PowerController
import app.tivi.util.SaveData
-import coil.intercept.Interceptor
-import coil.request.ImageRequest
-import coil.request.ImageResult
-import coil.size.pxOrElse
+import com.seiko.imageloader.intercept.Interceptor
+import com.seiko.imageloader.model.ImageRequest
+import com.seiko.imageloader.model.ImageResult
+import kotlin.math.roundToInt
import me.tatarka.inject.annotations.Inject
import org.mobilenativefoundation.store.store5.impl.extensions.get
@@ -22,6 +23,7 @@ class ShowCoilInterceptor(
private val tmdbImageUrlProvider: Lazy,
private val showImagesStore: ShowImagesStore,
private val powerController: PowerController,
+ private val density: () -> Density,
) : Interceptor {
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val request = when (val data = chain.request.data) {
@@ -40,15 +42,17 @@ class ShowCoilInterceptor(
}.getOrNull()
return if (entity != null) {
+ val size = chain.options.sizeResolver.run { density().size() }
+
val width = when (powerController.shouldSaveData()) {
- is SaveData.Disabled -> chain.size.width.pxOrElse { 0 }
+ is SaveData.Disabled -> size.width.roundToInt()
// If we can't download hi-res images, we load half-width images (so ~1/4 in size)
- is SaveData.Enabled -> chain.size.width.pxOrElse { 0 } / 2
+ is SaveData.Enabled -> size.width.roundToInt() / 2
}
- chain.request.newBuilder()
- .data(tmdbImageUrlProvider.value.buildUrl(entity, model.imageType, width))
- .build()
+ chain.request.newBuilder {
+ data(tmdbImageUrlProvider.value.buildUrl(entity, model.imageType, width))
+ }
} else {
chain.request
}
diff --git a/common/imageloading/src/main/java/app/tivi/common/imageloading/TmdbImageEntityCoilInterceptor.kt b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/TmdbImageEntityCoilInterceptor.kt
similarity index 67%
rename from common/imageloading/src/main/java/app/tivi/common/imageloading/TmdbImageEntityCoilInterceptor.kt
rename to common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/TmdbImageEntityCoilInterceptor.kt
index 84c7afcc89..b1346e5331 100644
--- a/common/imageloading/src/main/java/app/tivi/common/imageloading/TmdbImageEntityCoilInterceptor.kt
+++ b/common/imageloading/src/commonMain/kotlin/app/tivi/common/imageloading/TmdbImageEntityCoilInterceptor.kt
@@ -3,39 +3,44 @@
package app.tivi.common.imageloading
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.unit.Density
import app.tivi.data.models.TmdbImageEntity
import app.tivi.tmdb.TmdbImageUrlProvider
import app.tivi.util.PowerController
import app.tivi.util.SaveData
-import coil.intercept.Interceptor
-import coil.request.ImageResult
-import coil.size.Size
-import coil.size.pxOrElse
+import com.seiko.imageloader.intercept.Interceptor
+import com.seiko.imageloader.model.ImageResult
+import kotlin.math.roundToInt
import me.tatarka.inject.annotations.Inject
@Inject
class TmdbImageEntityCoilInterceptor(
private val tmdbImageUrlProvider: Lazy,
private val powerController: PowerController,
+ private val density: () -> Density,
) : Interceptor {
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
+ val size = chain.options.sizeResolver.run { density().size() }
+
val request = when (val data = chain.request.data) {
is TmdbImageEntity -> {
- chain.request.newBuilder()
- .data(map(data, chain.size))
- .build()
+ chain.request.newBuilder {
+ data(map(data, size))
+ }
}
else -> chain.request
}
+
return chain.proceed(request)
}
private fun map(data: TmdbImageEntity, size: Size): String {
val width = when (powerController.shouldSaveData()) {
- is SaveData.Disabled -> size.width.pxOrElse { 0 }
+ is SaveData.Disabled -> size.width.roundToInt()
// If we can't download hi-res images, we load half-width images (so ~1/4 in size)
- is SaveData.Enabled -> size.width.pxOrElse { 0 } / 2
+ is SaveData.Enabled -> size.width.roundToInt() / 2
}
return tmdbImageUrlProvider.value.buildUrl(data, data.type, width)
}
diff --git a/common/imageloading/src/iosMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt b/common/imageloading/src/iosMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt
new file mode 100644
index 0000000000..f8681eef22
--- /dev/null
+++ b/common/imageloading/src/iosMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt
@@ -0,0 +1,22 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.imageloading
+
+import app.tivi.util.Logger
+import com.seiko.imageloader.ImageLoader
+import com.seiko.imageloader.intercept.Interceptor
+import me.tatarka.inject.annotations.Provides
+
+actual interface ImageLoadingPlatformComponent {
+ @Provides
+ fun provideImageLoader(
+ interceptors: Set,
+ logger: Logger,
+ ): ImageLoader = IosImageLoaderFactory.create {
+ this.logger = logger.asImageLoaderLogger()
+ interceptor {
+ addInterceptors(interceptors)
+ }
+ }
+}
diff --git a/common/imageloading/src/iosMain/kotlin/app/tivi/common/imageloading/IosImageLoaderFactory.kt b/common/imageloading/src/iosMain/kotlin/app/tivi/common/imageloading/IosImageLoaderFactory.kt
new file mode 100644
index 0000000000..8dcfe52aa7
--- /dev/null
+++ b/common/imageloading/src/iosMain/kotlin/app/tivi/common/imageloading/IosImageLoaderFactory.kt
@@ -0,0 +1,48 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.imageloading
+
+import com.seiko.imageloader.ImageLoader
+import com.seiko.imageloader.ImageLoaderConfigBuilder
+import com.seiko.imageloader.cache.memory.maxSizePercent
+import com.seiko.imageloader.component.setupDefaultComponents
+import okio.Path
+import okio.Path.Companion.toPath
+import platform.Foundation.NSCachesDirectory
+import platform.Foundation.NSFileManager
+import platform.Foundation.NSUserDomainMask
+
+internal object IosImageLoaderFactory : ImageLoaderFactory {
+ private val cacheDir: Path by lazy {
+ NSFileManager.defaultManager.URLForDirectory(
+ directory = NSCachesDirectory,
+ inDomain = NSUserDomainMask,
+ appropriateForURL = null,
+ create = true,
+ error = null,
+ )!!.path.orEmpty().toPath()
+ }
+
+ override fun create(
+ block: ImageLoaderConfigBuilder.() -> Unit,
+ ): ImageLoader = ImageLoader {
+ // commonConfig()
+
+ components {
+ setupDefaultComponents(imageScope)
+ }
+ interceptor {
+ memoryCacheConfig {
+ // Set the max size to 25% of the app's available memory.
+ maxSizePercent(0.25)
+ }
+ diskCacheConfig {
+ directory(cacheDir.resolve("image_cache"))
+ maxSizeBytes(512L * 1024 * 1024) // 512MB
+ }
+ }
+
+ block()
+ }
+}
diff --git a/common/imageloading/src/jvmMain/kotlin/app/tivi/common/imageloading/DesktopImageLoaderFactory.kt b/common/imageloading/src/jvmMain/kotlin/app/tivi/common/imageloading/DesktopImageLoaderFactory.kt
new file mode 100644
index 0000000000..3547c65301
--- /dev/null
+++ b/common/imageloading/src/jvmMain/kotlin/app/tivi/common/imageloading/DesktopImageLoaderFactory.kt
@@ -0,0 +1,58 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.imageloading
+
+import com.seiko.imageloader.ImageLoader
+import com.seiko.imageloader.ImageLoaderConfigBuilder
+import com.seiko.imageloader.cache.memory.maxSizePercent
+import com.seiko.imageloader.component.setupDefaultComponents
+import java.io.File
+import okio.Path.Companion.toOkioPath
+
+internal object DesktopImageLoaderFactory : ImageLoaderFactory {
+ private fun getCacheDir(): File = when (currentOperatingSystem) {
+ OperatingSystem.Windows -> File(System.getenv("AppData"), "tivi/cache")
+ OperatingSystem.Linux -> File(System.getProperty("user.home"), ".cache/tivi")
+ OperatingSystem.MacOS -> File(System.getProperty("user.home"), "Library/Caches/tivi")
+ else -> throw IllegalStateException("Unsupported operating system")
+ }
+
+ override fun create(
+ block: ImageLoaderConfigBuilder.() -> Unit,
+ ): ImageLoader = ImageLoader {
+ components {
+ setupDefaultComponents(imageScope)
+ }
+ interceptor {
+ memoryCacheConfig {
+ // Set the max size to 25% of the app's available memory.
+ maxSizePercent(0.25)
+ }
+ diskCacheConfig {
+ directory(getCacheDir().resolve("image_cache").toOkioPath())
+ maxSizeBytes(512L * 1024 * 1024) // 512MB
+ }
+ }
+
+ block()
+ }
+}
+
+internal enum class OperatingSystem {
+ Windows, Linux, MacOS, Unknown
+}
+
+private val currentOperatingSystem: OperatingSystem
+ get() {
+ val os = System.getProperty("os.name").lowercase()
+ return when {
+ os.contains("win") -> OperatingSystem.Windows
+ os.contains("nix") || os.contains("nux") || os.contains("aix") -> {
+ OperatingSystem.Linux
+ }
+
+ os.contains("mac") -> OperatingSystem.MacOS
+ else -> OperatingSystem.Unknown
+ }
+ }
diff --git a/common/imageloading/src/jvmMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt b/common/imageloading/src/jvmMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt
new file mode 100644
index 0000000000..6665536301
--- /dev/null
+++ b/common/imageloading/src/jvmMain/kotlin/app/tivi/common/imageloading/ImageLoadingPlatformComponent.kt
@@ -0,0 +1,22 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.imageloading
+
+import app.tivi.util.Logger
+import com.seiko.imageloader.ImageLoader
+import com.seiko.imageloader.intercept.Interceptor
+import me.tatarka.inject.annotations.Provides
+
+actual interface ImageLoadingPlatformComponent {
+ @Provides
+ fun provideImageLoader(
+ interceptors: Set,
+ logger: Logger,
+ ): ImageLoader = DesktopImageLoaderFactory.create {
+ this.logger = logger.asImageLoaderLogger()
+ interceptor {
+ addInterceptors(interceptors)
+ }
+ }
+}
diff --git a/common/imageloading/src/main/java/app/tivi/common/imageloading/CoilAppInitializer.kt b/common/imageloading/src/main/java/app/tivi/common/imageloading/CoilAppInitializer.kt
deleted file mode 100644
index ce1955174f..0000000000
--- a/common/imageloading/src/main/java/app/tivi/common/imageloading/CoilAppInitializer.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2019, Google LLC, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package app.tivi.common.imageloading
-
-import android.app.Application
-import app.tivi.appinitializers.AppInitializer
-import coil.Coil
-import coil.ImageLoader
-import me.tatarka.inject.annotations.Inject
-import okhttp3.OkHttpClient
-
-@Inject
-class CoilAppInitializer(
- private val application: Application,
- private val showImageInterceptor: ShowCoilInterceptor,
- private val episodeEntityInterceptor: EpisodeCoilInterceptor,
- private val tmdbImageEntityInterceptor: TmdbImageEntityCoilInterceptor,
- private val okHttpClient: OkHttpClient,
-) : AppInitializer {
- override fun init() {
- Coil.setImageLoader {
- ImageLoader.Builder(application)
- .components {
- add(showImageInterceptor)
- add(episodeEntityInterceptor)
- add(tmdbImageEntityInterceptor)
- }
- .okHttpClient(okHttpClient)
- .build()
- }
- }
-}
diff --git a/common/imageloading/src/main/java/app/tivi/common/imageloading/ImageLoadingComponent.kt b/common/imageloading/src/main/java/app/tivi/common/imageloading/ImageLoadingComponent.kt
deleted file mode 100644
index 7682cc33e4..0000000000
--- a/common/imageloading/src/main/java/app/tivi/common/imageloading/ImageLoadingComponent.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright 2019, Google LLC, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package app.tivi.common.imageloading
-
-import app.tivi.appinitializers.AppInitializer
-import me.tatarka.inject.annotations.IntoSet
-import me.tatarka.inject.annotations.Provides
-
-interface ImageLoadingComponent {
- @Provides
- @IntoSet
- fun provideCoilInitializer(bind: CoilAppInitializer): AppInitializer = bind
-}
diff --git a/common/imageloading/src/main/java/app/tivi/common/imageloading/TrimTransparentEdgesTransformation.kt b/common/imageloading/src/main/java/app/tivi/common/imageloading/TrimTransparentEdgesTransformation.kt
deleted file mode 100644
index b2b4537b4e..0000000000
--- a/common/imageloading/src/main/java/app/tivi/common/imageloading/TrimTransparentEdgesTransformation.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright 2019, Google LLC, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package app.tivi.common.imageloading
-
-import android.graphics.Bitmap
-import android.graphics.Canvas
-import android.graphics.Rect
-import androidx.core.graphics.alpha
-import androidx.core.graphics.createBitmap
-import coil.size.Size
-import coil.transform.Transformation
-
-/**
- * A [Transformation] that trims transparent edges from an image.
- */
-object TrimTransparentEdgesTransformation : Transformation {
- override val cacheKey: String = "TrimTransparentEdgesTransformation"
-
- override suspend fun transform(input: Bitmap, size: Size): Bitmap {
- val inputWidth = input.width
- val inputHeight = input.height
-
- var firstX = 0
- var firstY = 0
- var lastX = inputWidth
- var lastY = inputHeight
-
- val pixels = IntArray(inputWidth * inputHeight)
- input.getPixels(pixels, 0, inputWidth, 0, 0, inputWidth, inputHeight)
-
- loop@
- for (x in 0 until inputWidth) {
- for (y in 0 until inputHeight) {
- if (pixels[x + y * inputWidth].alpha > 0) {
- firstX = x
- break@loop
- }
- }
- }
-
- loop@
- for (y in 0 until inputHeight) {
- for (x in firstX until inputWidth) {
- if (pixels[x + y * inputWidth].alpha > 0) {
- firstY = y
- break@loop
- }
- }
- }
-
- loop@
- for (x in inputWidth - 1 downTo firstX) {
- for (y in inputHeight - 1 downTo firstY) {
- if (pixels[x + y * inputWidth].alpha > 0) {
- lastX = x
- break@loop
- }
- }
- }
-
- loop@
- for (y in inputHeight - 1 downTo firstY) {
- for (x in inputWidth - 1 downTo firstX) {
- if (pixels[x + y * inputWidth].alpha > 0) {
- lastY = y
- break@loop
- }
- }
- }
-
- if (firstX == 0 && firstY == 0 && lastX == inputWidth && lastY == inputHeight) {
- return input
- }
-
- val output = createBitmap(
- width = 1 + lastX - firstX,
- height = 1 + lastY - firstY,
- config = Bitmap.Config.ARGB_8888,
- )
- val canvas = Canvas(output)
-
- val src = Rect(firstX, firstY, firstX + output.width, firstY + output.height)
- val dst = Rect(0, 0, output.width, output.height)
-
- canvas.drawBitmap(input, src, dst, null)
-
- return output
- }
-}
diff --git a/common/ui/circuit-overlay/build.gradle.kts b/common/ui/circuit-overlay/build.gradle.kts
index eee1660dcf..6c900c5c4e 100644
--- a/common/ui/circuit-overlay/build.gradle.kts
+++ b/common/ui/circuit-overlay/build.gradle.kts
@@ -4,24 +4,29 @@
plugins {
id("app.tivi.android.library")
- id("app.tivi.android.compose")
- id("app.tivi.kotlin.android")
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
}
android {
namespace = "app.tivi.ui.overlays"
}
-dependencies {
- implementation(projects.common.ui.compose)
- implementation(projects.common.ui.screens)
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(projects.common.ui.compose)
+ implementation(projects.common.ui.screens)
- implementation(platform(libs.compose.bom))
- implementation(libs.compose.material3.material3)
- implementation(libs.compose.animation.animation)
- implementation(libs.compose.ui.tooling)
- implementation(libs.androidx.activity.compose)
+ implementation(compose.material3)
+ implementation(compose.animation)
- api(libs.circuit.foundation)
- api(libs.circuit.overlay)
+ implementation(libs.materialdialogs.core)
+
+ api(libs.circuit.foundation)
+ api(libs.circuit.overlay)
+ }
+ }
+ }
}
diff --git a/common/ui/circuit-overlay/src/commonMain/kotlin/app/tivi/overlays/BottomSheetOverlay.kt b/common/ui/circuit-overlay/src/commonMain/kotlin/app/tivi/overlays/BottomSheetOverlay.kt
new file mode 100644
index 0000000000..d72e76421f
--- /dev/null
+++ b/common/ui/circuit-overlay/src/commonMain/kotlin/app/tivi/overlays/BottomSheetOverlay.kt
@@ -0,0 +1,62 @@
+// Copyright (C) 2022 Slack Technologies, LLC
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.overlays
+
+// @OptIn(ExperimentalMaterial3Api::class)
+// class BottomSheetOverlay(
+// private val model: Model,
+// private val onDismiss: () -> Result,
+// private val tonalElevation: Dp = BottomSheetDefaults.Elevation,
+// private val scrimColor: Color = Color.Unspecified,
+// private val content: @Composable (Model, OverlayNavigator) -> Unit,
+// ) : Overlay {
+// @Composable
+// override fun Content(navigator: OverlayNavigator) {
+// val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+//
+// val coroutineScope = rememberCoroutineScope()
+// BackHandler(enabled = sheetState.isVisible) {
+// coroutineScope
+// .launch { sheetState.hide() }
+// .invokeOnCompletion {
+// if (!sheetState.isVisible) {
+// navigator.finish(onDismiss())
+// }
+// }
+// }
+//
+// ModalBottomSheet(
+// modifier = Modifier.fillMaxWidth(),
+// content = {
+// // Delay setting the result until we've finished dismissing
+// content(model) { result ->
+// // This is the OverlayNavigator.finish() callback
+// coroutineScope.launch {
+// try {
+// sheetState.hide()
+// } finally {
+// navigator.finish(result)
+// }
+// }
+// }
+// },
+// tonalElevation = tonalElevation,
+// scrimColor = if (scrimColor.isSpecified) scrimColor else BottomSheetDefaults.ScrimColor,
+// sheetState = sheetState,
+// onDismissRequest = { navigator.finish(onDismiss()) },
+// )
+//
+// LaunchedEffect(Unit) { sheetState.show() }
+// }
+// }
+//
+// suspend fun OverlayHost.showInBottomSheet(
+// screen: Screen,
+// ): Unit = show(
+// BottomSheetOverlay(Unit, {}) { _, _ ->
+// // We want to use `onNavEvent` here to finish the overlay but we're blocked by
+// // https://github.com/slackhq/circuit/issues/653
+// CircuitContent(screen = screen)
+// },
+// )
diff --git a/common/ui/circuit-overlay/src/main/kotlin/app/tivi/overlays/DialogOverlay.kt b/common/ui/circuit-overlay/src/commonMain/kotlin/app/tivi/overlays/DialogOverlay.kt
similarity index 58%
rename from common/ui/circuit-overlay/src/main/kotlin/app/tivi/overlays/DialogOverlay.kt
rename to common/ui/circuit-overlay/src/commonMain/kotlin/app/tivi/overlays/DialogOverlay.kt
index 9bb80e863c..8f82e30495 100644
--- a/common/ui/circuit-overlay/src/main/kotlin/app/tivi/overlays/DialogOverlay.kt
+++ b/common/ui/circuit-overlay/src/commonMain/kotlin/app/tivi/overlays/DialogOverlay.kt
@@ -3,18 +3,14 @@
package app.tivi.overlays
-import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.window.Dialog
-import androidx.compose.ui.window.DialogProperties
import app.tivi.common.compose.rememberCoroutineScope
-import app.tivi.common.compose.ui.androidMinWidthDialogSize
import com.slack.circuit.foundation.CircuitContent
import com.slack.circuit.overlay.Overlay
import com.slack.circuit.overlay.OverlayHost
import com.slack.circuit.overlay.OverlayNavigator
import com.slack.circuit.runtime.Screen
+import com.vanpra.composematerialdialogs.MaterialDialog
import kotlinx.coroutines.launch
class DialogOverlay(
@@ -25,17 +21,14 @@ class DialogOverlay(
@Composable
override fun Content(navigator: OverlayNavigator) {
val coroutineScope = rememberCoroutineScope()
- Dialog(
- onDismissRequest = { navigator.finish(onDismiss()) },
- properties = DialogProperties(usePlatformDefaultWidth = false),
+ MaterialDialog(
+ onCloseRequest = { navigator.finish(onDismiss()) },
) {
- Box(Modifier.androidMinWidthDialogSize(clampMaxWidth = true)) {
- // Delay setting the result until we've finished dismissing
- content(model) { result ->
- // This is the OverlayNavigator.finish() callback
- coroutineScope.launch {
- navigator.finish(result)
- }
+ // Delay setting the result until we've finished dismissing
+ content(model) { result ->
+ // This is the OverlayNavigator.finish() callback
+ coroutineScope.launch {
+ navigator.finish(result)
}
}
}
diff --git a/common/ui/circuit-overlay/src/main/kotlin/app/tivi/overlays/LocalNavigator.kt b/common/ui/circuit-overlay/src/commonMain/kotlin/app/tivi/overlays/LocalNavigator.kt
similarity index 100%
rename from common/ui/circuit-overlay/src/main/kotlin/app/tivi/overlays/LocalNavigator.kt
rename to common/ui/circuit-overlay/src/commonMain/kotlin/app/tivi/overlays/LocalNavigator.kt
diff --git a/common/ui/circuit-overlay/src/main/kotlin/app/tivi/overlays/BottomSheetOverlay.kt b/common/ui/circuit-overlay/src/main/kotlin/app/tivi/overlays/BottomSheetOverlay.kt
deleted file mode 100644
index 2580463cc9..0000000000
--- a/common/ui/circuit-overlay/src/main/kotlin/app/tivi/overlays/BottomSheetOverlay.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright (C) 2022 Slack Technologies, LLC
-// SPDX-License-Identifier: Apache-2.0
-
-package app.tivi.overlays
-
-import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.material3.BottomSheetDefaults
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ModalBottomSheet
-import androidx.compose.material3.rememberModalBottomSheetState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.isSpecified
-import androidx.compose.ui.unit.Dp
-import app.tivi.common.compose.rememberCoroutineScope
-import com.slack.circuit.foundation.CircuitContent
-import com.slack.circuit.overlay.Overlay
-import com.slack.circuit.overlay.OverlayHost
-import com.slack.circuit.overlay.OverlayNavigator
-import com.slack.circuit.runtime.Screen
-import kotlinx.coroutines.launch
-
-@OptIn(ExperimentalMaterial3Api::class)
-class BottomSheetOverlay(
- private val model: Model,
- private val onDismiss: () -> Result,
- private val tonalElevation: Dp = BottomSheetDefaults.Elevation,
- private val scrimColor: Color = Color.Unspecified,
- private val content: @Composable (Model, OverlayNavigator) -> Unit,
-) : Overlay {
- @Composable
- override fun Content(navigator: OverlayNavigator) {
- val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
-
- val coroutineScope = rememberCoroutineScope()
- BackHandler(enabled = sheetState.isVisible) {
- coroutineScope
- .launch { sheetState.hide() }
- .invokeOnCompletion {
- if (!sheetState.isVisible) {
- navigator.finish(onDismiss())
- }
- }
- }
-
- ModalBottomSheet(
- modifier = Modifier.fillMaxWidth(),
- content = {
- // Delay setting the result until we've finished dismissing
- content(model) { result ->
- // This is the OverlayNavigator.finish() callback
- coroutineScope.launch {
- try {
- sheetState.hide()
- } finally {
- navigator.finish(result)
- }
- }
- }
- },
- tonalElevation = tonalElevation,
- scrimColor = if (scrimColor.isSpecified) scrimColor else BottomSheetDefaults.ScrimColor,
- sheetState = sheetState,
- onDismissRequest = { navigator.finish(onDismiss()) },
- )
-
- LaunchedEffect(Unit) { sheetState.show() }
- }
-}
-
-suspend fun OverlayHost.showInBottomSheet(
- screen: Screen,
-): Unit = show(
- BottomSheetOverlay(Unit, {}) { _, _ ->
- // We want to use `onNavEvent` here to finish the overlay but we're blocked by
- // https://github.com/slackhq/circuit/issues/653
- CircuitContent(screen = screen)
- },
-)
diff --git a/common/ui/compose/build.gradle.kts b/common/ui/compose/build.gradle.kts
index 7dd719cb77..7a21ed647d 100644
--- a/common/ui/compose/build.gradle.kts
+++ b/common/ui/compose/build.gradle.kts
@@ -4,47 +4,53 @@
plugins {
id("app.tivi.android.library")
- id("app.tivi.android.compose")
- id("app.tivi.kotlin.android")
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
}
-android {
- namespace = "app.tivi.common.compose"
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ api(projects.data.models)
+ api(projects.core.preferences)
+ api(projects.common.imageloading)
- buildFeatures {
- buildConfig = true
- }
+ api(projects.common.ui.screens)
+ api(libs.circuit.foundation)
- lint {
- baseline = file("lint-baseline.xml")
- }
-}
+ api(projects.common.ui.resources)
+ api(libs.moko.resourcesCompose)
-dependencies {
- api(projects.data.models)
- api(projects.core.preferences)
- api(projects.common.imageloading)
+ implementation(compose.foundation)
+ implementation(compose.material)
+ implementation(compose.materialIconsExtended)
+ api(compose.material3)
+ api(libs.compose.material3.windowsizeclass)
+ implementation(compose.animation)
- api(projects.common.ui.screens)
- api(libs.circuit.foundation)
+ api(libs.insetsx)
- api(projects.common.ui.resources)
- api(projects.common.ui.resourcesCompose)
+ implementation(libs.materialdialogs.core)
- implementation(libs.androidx.core)
+ implementation(libs.uuid)
- api(platform(libs.compose.bom))
- implementation(libs.compose.ui.ui)
- implementation(libs.compose.foundation.foundation)
- implementation(libs.compose.foundation.layout)
- implementation(libs.compose.material.material)
- implementation(libs.compose.material.iconsext)
- api(libs.compose.material3.material3)
- api(libs.compose.material3.windowsizeclass)
- implementation(libs.compose.animation.animation)
- implementation(libs.compose.ui.tooling)
+ implementation(libs.paging.compose)
+ }
+ }
- implementation(libs.paging.compose)
+ val androidMain by getting {
+ dependencies {
+ implementation(libs.androidx.activity.compose)
+ }
+ }
+ }
+}
+
+android {
+ namespace = "app.tivi.common.compose"
- implementation(libs.coil.compose)
+ lint {
+ baseline = file("lint-baseline.xml")
+ }
}
diff --git a/common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt b/common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt
new file mode 100644
index 0000000000..cba637b33c
--- /dev/null
+++ b/common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt
@@ -0,0 +1,16 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.compose
+
+import android.os.Build
+import androidx.compose.runtime.Composable
+
+@Composable
+actual fun ReportDrawnWhen(predicate: () -> Boolean) {
+ // ReportDrawnWhen routinely causes crashes on API < 25:
+ // https://issuetracker.google.com/issues/260506820
+ if (Build.VERSION.SDK_INT >= 25) {
+ androidx.activity.compose.ReportDrawnWhen(predicate)
+ }
+}
diff --git a/common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/theme/Platform.kt b/common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/theme/Platform.kt
new file mode 100644
index 0000000000..74b009de42
--- /dev/null
+++ b/common/ui/compose/src/androidMain/kotlin/app/tivi/common/compose/theme/Platform.kt
@@ -0,0 +1,26 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.compose.theme
+
+import android.os.Build
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+@Composable
+internal actual fun colorScheme(
+ useDarkColors: Boolean,
+ useDynamicColors: Boolean,
+): ColorScheme = when {
+ Build.VERSION.SDK_INT >= 31 && useDynamicColors && useDarkColors -> {
+ dynamicDarkColorScheme(LocalContext.current)
+ }
+ Build.VERSION.SDK_INT >= 31 && useDynamicColors && !useDarkColors -> {
+ dynamicLightColorScheme(LocalContext.current)
+ }
+ useDarkColors -> TiviDarkColors
+ else -> TiviLightColors
+}
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/EntryGrid.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/EntryGrid.kt
similarity index 94%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/EntryGrid.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/EntryGrid.kt
index ee22ddccbf..1bbea723b8 100644
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/EntryGrid.kt
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/EntryGrid.kt
@@ -14,14 +14,16 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.material.DismissValue
import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
+import androidx.compose.material.rememberDismissState
import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.DismissValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -29,12 +31,10 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.SwipeToDismiss
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
-import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
@@ -42,7 +42,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp
-import androidx.paging.LoadState
+import app.cash.paging.LoadStateLoading
import app.cash.paging.compose.LazyPagingItems
import app.tivi.common.compose.ui.PlaceholderPosterCard
import app.tivi.common.compose.ui.PosterCard
@@ -66,16 +66,14 @@ fun EntryGrid(
val snackbarHostState = remember { SnackbarHostState() }
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
- val dismissSnackbarState = rememberDismissState(
- confirmValueChange = { value ->
- if (value != DismissValue.Default) {
- snackbarHostState.currentSnackbarData?.dismiss()
- true
- } else {
- false
- }
- },
- )
+ val dismissSnackbarState = rememberDismissState { value ->
+ if (value != DismissValue.Default) {
+ snackbarHostState.currentSnackbarData?.dismiss()
+ true
+ } else {
+ false
+ }
+ }
lazyPagingItems.loadState.prependErrorOrNull()?.let { message ->
LaunchedEffect(message) {
@@ -98,7 +96,7 @@ fun EntryGrid(
EntryGridAppBar(
title = title,
onNavigateUp = onNavigateUp,
- refreshing = lazyPagingItems.loadState.refresh == LoadState.Loading,
+ refreshing = lazyPagingItems.loadState.refresh == LoadStateLoading,
onRefreshActionClick = { lazyPagingItems.refresh() },
modifier = Modifier.fillMaxWidth(),
scrollBehavior = scrollBehavior,
@@ -118,7 +116,7 @@ fun EntryGrid(
},
modifier = modifier,
) { paddingValues ->
- val refreshing = lazyPagingItems.loadState.refresh == LoadState.Loading
+ val refreshing = lazyPagingItems.loadState.refresh == LoadStateLoading
val refreshState = rememberPullRefreshState(
refreshing = refreshing,
onRefresh = lazyPagingItems::refresh,
@@ -159,7 +157,7 @@ fun EntryGrid(
}
}
- if (lazyPagingItems.loadState.append == LoadState.Loading) {
+ if (lazyPagingItems.loadState.append == LoadStateLoading) {
fullSpanItem {
Box(
Modifier
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/Layout.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/Layout.kt
similarity index 96%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/Layout.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/Layout.kt
index b5f41b7409..fcb839bffc 100644
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/Layout.kt
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/Layout.kt
@@ -9,13 +9,13 @@ import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.systemBars
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import com.moriatsushi.insetsx.systemBars
object Layout {
val bodyMargin: Dp
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/LazyList.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/LazyList.kt
similarity index 88%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/LazyList.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/LazyList.kt
index 00a393aa47..ddea60e8d4 100644
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/LazyList.kt
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/LazyList.kt
@@ -5,9 +5,6 @@
package app.tivi.common.compose
-import android.annotation.SuppressLint
-import android.os.Parcel
-import android.os.Parcelable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@@ -107,23 +104,7 @@ fun LazyGridScope.items(
}
}
-@SuppressLint("BanParcelableUsage")
-internal data class PagingPlaceholderKey(private val index: Int) : Parcelable {
- override fun writeToParcel(parcel: Parcel, flags: Int) = parcel.writeInt(index)
- override fun describeContents(): Int = 0
-
- companion object {
- @Suppress("unused")
- @JvmField
- val CREATOR: Parcelable.Creator =
- object : Parcelable.Creator {
- override fun createFromParcel(parcel: Parcel) =
- PagingPlaceholderKey(parcel.readInt())
-
- override fun newArray(size: Int) = arrayOfNulls(size)
- }
- }
-}
+internal data class PagingPlaceholderKey(private val index: Int)
inline fun LazyGridScope.fullSpanItem(
key: Any? = null,
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/LazyPagingExtensions.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/LazyPagingExtensions.kt
similarity index 100%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/LazyPagingExtensions.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/LazyPagingExtensions.kt
diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt
new file mode 100644
index 0000000000..0c7d89e67b
--- /dev/null
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt
@@ -0,0 +1,9 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.compose
+
+import androidx.compose.runtime.Composable
+
+@Composable
+expect fun ReportDrawnWhen(predicate: () -> Boolean)
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/StableCoroutineScope.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/StableCoroutineScope.kt
similarity index 100%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/StableCoroutineScope.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/StableCoroutineScope.kt
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/TiviCompositionLocals.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/TiviCompositionLocals.kt
similarity index 100%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/TiviCompositionLocals.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/TiviCompositionLocals.kt
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/TiviPreferenceExtensions.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/TiviPreferenceExtensions.kt
similarity index 100%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/TiviPreferenceExtensions.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/TiviPreferenceExtensions.kt
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/UiMessage.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/UiMessage.kt
similarity index 89%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/UiMessage.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/UiMessage.kt
index be127a2365..0e723106be 100644
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/UiMessage.kt
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/UiMessage.kt
@@ -3,7 +3,7 @@
package app.tivi.common.compose
-import java.util.UUID
+import com.benasher44.uuid.uuid4
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -13,12 +13,12 @@ import kotlinx.coroutines.sync.withLock
data class UiMessage(
val message: String,
- val id: Long = UUID.randomUUID().mostSignificantBits,
+ val id: Long = uuid4().mostSignificantBits,
)
fun UiMessage(
t: Throwable,
- id: Long = UUID.randomUUID().mostSignificantBits,
+ id: Long = uuid4().mostSignificantBits,
): UiMessage = UiMessage(
message = t.message ?: "Error occurred: $t",
id = id,
diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/WindowSizeClass.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/WindowSizeClass.kt
new file mode 100644
index 0000000000..bc24f9c75d
--- /dev/null
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/WindowSizeClass.kt
@@ -0,0 +1,11 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.compose
+
+import androidx.compose.material3.windowsizeclass.WindowSizeClass
+import androidx.compose.runtime.staticCompositionLocalOf
+
+val LocalWindowSizeClass = staticCompositionLocalOf {
+ error("No WindowSizeClass available")
+}
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/theme/Color.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Color.kt
similarity index 100%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/theme/Color.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Color.kt
diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Platform.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Platform.kt
new file mode 100644
index 0000000000..e0d106cdf2
--- /dev/null
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Platform.kt
@@ -0,0 +1,13 @@
+// Copyright 2020, Google LLC, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.compose.theme
+
+import androidx.compose.material3.ColorScheme
+import androidx.compose.runtime.Composable
+
+@Composable
+internal expect fun colorScheme(
+ useDarkColors: Boolean,
+ useDynamicColors: Boolean,
+): ColorScheme
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/theme/Shape.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Shape.kt
similarity index 100%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/theme/Shape.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Shape.kt
diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Theme.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Theme.kt
new file mode 100644
index 0000000000..dc51ccf6e8
--- /dev/null
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Theme.kt
@@ -0,0 +1,22 @@
+// Copyright 2020, Google LLC, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.compose.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+
+@Composable
+fun TiviTheme(
+ useDarkColors: Boolean = isSystemInDarkTheme(),
+ useDynamicColors: Boolean = false,
+ content: @Composable () -> Unit,
+) {
+ MaterialTheme(
+ colorScheme = colorScheme(useDarkColors, useDynamicColors),
+ typography = TiviTypography,
+ shapes = TiviShapes,
+ content = content,
+ )
+}
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/theme/Type.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Type.kt
similarity index 100%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/theme/Type.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/theme/Type.kt
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/AppBar.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/AppBar.kt
similarity index 98%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/AppBar.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/AppBar.kt
index 3dce9f5a94..95fdfb6406 100644
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/AppBar.kt
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/AppBar.kt
@@ -51,7 +51,7 @@ fun TopAppBarWithBottomContent(
title = title,
navigationIcon = navigationIcon,
actions = actions,
- colors = TopAppBarDefaults.topAppBarColors(
+ colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = Color.Transparent,
titleContentColor = LocalContentColor.current,
actionIconContentColor = LocalContentColor.current,
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/AutoSizedCircularProgressIndicator.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/AutoSizedCircularProgressIndicator.kt
similarity index 64%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/AutoSizedCircularProgressIndicator.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/AutoSizedCircularProgressIndicator.kt
index 99522e5d7d..dadcccf7fe 100644
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/AutoSizedCircularProgressIndicator.kt
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/AutoSizedCircularProgressIndicator.kt
@@ -4,16 +4,12 @@
package app.tivi.common.compose.ui
import androidx.compose.foundation.layout.BoxWithConstraints
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.coerceAtLeast
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.min
@@ -49,31 +45,3 @@ private val DefaultDiameter = 40.dp
private val InternalPadding = 4.dp
private val StrokeDiameterFraction = DefaultStrokeWidth / DefaultDiameter
-
-@Preview
-@Composable
-fun PreviewAutoSizedCircularProgressIndicator() {
- Surface {
- Column {
- AutoSizedCircularProgressIndicator(
- modifier = Modifier.size(16.dp),
- )
-
- AutoSizedCircularProgressIndicator(
- modifier = Modifier.size(24.dp),
- )
-
- AutoSizedCircularProgressIndicator(
- modifier = Modifier.size(48.dp),
- )
-
- AutoSizedCircularProgressIndicator(
- modifier = Modifier.size(64.dp),
- )
-
- AutoSizedCircularProgressIndicator(
- modifier = Modifier.size(128.dp),
- )
- }
- }
-}
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/Backdrop.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Backdrop.kt
similarity index 98%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/Backdrop.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Backdrop.kt
index 6f6b127a43..cf09cd326b 100644
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/Backdrop.kt
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Backdrop.kt
@@ -41,7 +41,6 @@ fun Backdrop(
if (imageModel != null) {
AsyncImage(
model = imageModel,
- requestBuilder = { crossfade(true) },
contentDescription = stringResource(MR.strings.cd_show_poster),
contentScale = ContentScale.Crop,
modifier = Modifier
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/DateTimeTextFields.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/DateTimeTextFields.kt
similarity index 56%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/DateTimeTextFields.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/DateTimeTextFields.kt
index 6fd1980352..33409419e7 100644
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/DateTimeTextFields.kt
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/DateTimeTextFields.kt
@@ -3,23 +3,16 @@
package app.tivi.common.compose.ui
-import android.text.format.DateFormat
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.DatePicker
-import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
import androidx.compose.material3.TextFieldDefaults
-import androidx.compose.material3.TimePicker
-import androidx.compose.material3.rememberDatePickerState
-import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
@@ -28,20 +21,18 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import app.tivi.common.compose.LocalTiviDateFormatter
import app.tivi.common.ui.resources.MR
import dev.icerock.moko.resources.compose.stringResource
-import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
-import kotlinx.datetime.toLocalDateTime
@OptIn(ExperimentalMaterial3Api::class)
+@Suppress("UNUSED_PARAMETER")
@Composable
fun DateTextField(
selectedDate: LocalDate?,
@@ -64,39 +55,39 @@ fun DateTextField(
)
if (showPicker) {
- val datePickerState = rememberDatePickerState(
- initialSelectedDateMillis = selectedDate?.let { date ->
- remember { date.toEpochMillis() }
- },
- )
-
- DatePickerDialog(
- onDismissRequest = { showPicker = false },
- confirmButton = {
- TextButton(
- onClick = {
- showPicker = false
-
- datePickerState.selectedDateMillis?.let { millis ->
- val date = Instant.fromEpochMilliseconds(millis)
- .toLocalDateTime(TimeZone.currentSystemDefault())
- .date
- onDateSelected(date)
- }
- },
- ) {
- Text("Confirm")
- }
- },
- ) {
- DatePicker(
- state = datePickerState,
- dateValidator = { epoch ->
- // Only allow dates in the past
- epoch < System.currentTimeMillis()
- },
- )
- }
+// val datePickerState = rememberDatePickerState(
+// initialSelectedDateMillis = selectedDate?.let { date ->
+// remember { date.toEpochMillis() }
+// },
+// )
+//
+// DatePickerDialog(
+// onDismissRequest = { showPicker = false },
+// confirmButton = {
+// TextButton(
+// onClick = {
+// showPicker = false
+//
+// datePickerState.selectedDateMillis?.let { millis ->
+// val date = Instant.fromEpochMilliseconds(millis)
+// .toLocalDateTime(TimeZone.currentSystemDefault())
+// .date
+// onDateSelected(date)
+// }
+// },
+// ) {
+// Text("Confirm")
+// }
+// },
+// ) {
+// DatePicker(
+// state = datePickerState,
+// dateValidator = { epoch ->
+// // Only allow dates in the past
+// epoch < System.currentTimeMillis()
+// },
+// )
+// }
}
}
}
@@ -110,12 +101,13 @@ private fun LocalDate.toEpochMillis(): Long {
private val midday: LocalTime = LocalTime(12, 0, 0, 0)
@OptIn(ExperimentalMaterial3Api::class)
+@Suppress("UNUSED_PARAMETER")
@Composable
fun TimeTextField(
selectedTime: LocalTime?,
onTimeSelected: (LocalTime) -> Unit,
modifier: Modifier = Modifier,
- is24Hour: Boolean = TimeTextFieldDefaults.is24Hour,
+ is24Hour: Boolean = true, // FIXME
) {
var showPicker by remember { mutableStateOf(false) }
@@ -133,48 +125,48 @@ fun TimeTextField(
)
if (showPicker) {
- val timePickerState = rememberTimePickerState(
- initialHour = selectedTime?.hour ?: 0,
- initialMinute = selectedTime?.minute ?: 0,
- is24Hour = is24Hour,
- )
-
- TimePickerDialog(
- onDismissRequest = { showPicker = false },
- confirmButton = {
- TextButton(
- onClick = {
- showPicker = false
-
- onTimeSelected(
- LocalTime(
- hour = timePickerState.hour,
- minute = timePickerState.minute,
- second = 0,
- nanosecond = 0,
- ),
- )
- },
- ) {
- Text(text = "Confirm")
- }
- },
- ) {
- Box(Modifier.padding(24.dp)) {
- TimePicker(state = timePickerState)
- }
- }
+// val timePickerState = rememberTimePickerState(
+// initialHour = selectedTime?.hour ?: 0,
+// initialMinute = selectedTime?.minute ?: 0,
+// is24Hour = is24Hour,
+// )
+//
+// TimePickerDialog(
+// onDismissRequest = { showPicker = false },
+// confirmButton = {
+// TextButton(
+// onClick = {
+// showPicker = false
+//
+// onTimeSelected(
+// LocalTime(
+// hour = timePickerState.hour,
+// minute = timePickerState.minute,
+// second = 0,
+// nanosecond = 0,
+// ),
+// )
+// },
+// ) {
+// Text(text = "Confirm")
+// }
+// },
+// ) {
+// Box(Modifier.padding(24.dp)) {
+// TimePicker(state = timePickerState)
+// }
+// }
}
}
}
-object TimeTextFieldDefaults {
- val is24Hour: Boolean
- @Composable get() {
- val context = LocalContext.current
- return remember { DateFormat.is24HourFormat(context) }
- }
-}
+// object TimeTextFieldDefaults {
+// val is24Hour: Boolean
+// @Composable get() {
+// val context = LocalContext.current
+// return remember { DateFormat.is24HourFormat(context) }
+// }
+// }
@ExperimentalMaterial3Api
@Composable
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/Empty.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Empty.kt
similarity index 100%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/Empty.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Empty.kt
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/ExpandingSummary.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/ExpandingSummary.kt
similarity index 100%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/ExpandingSummary.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/ExpandingSummary.kt
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/GradientScrim.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/GradientScrim.kt
similarity index 91%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/GradientScrim.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/GradientScrim.kt
index 0b303ff813..69d50c6c36 100644
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/GradientScrim.kt
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/GradientScrim.kt
@@ -3,7 +3,6 @@
package app.tivi.common.compose.ui
-import androidx.annotation.FloatRange
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
@@ -26,8 +25,8 @@ fun Modifier.drawForegroundGradientScrim(
color: Color,
decay: Float = 1.5f,
numStops: Int = 16,
- @FloatRange(from = 0.0, to = 1.0) startY: Float = 0f,
- @FloatRange(from = 0.0, to = 1.0) endY: Float = 1f,
+ startY: Float = 0f,
+ endY: Float = 1f,
): Modifier = composed {
val colors = remember(color, numStops) {
val baseAlpha = color.alpha
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/IconButtonScrim.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/IconButtonScrim.kt
similarity index 95%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/IconButtonScrim.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/IconButtonScrim.kt
index 07136f925a..d0490fb1d6 100644
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/IconButtonScrim.kt
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/IconButtonScrim.kt
@@ -3,7 +3,6 @@
package app.tivi.common.compose.ui
-import androidx.annotation.FloatRange
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
@@ -47,7 +46,7 @@ fun ScrimmedIconButton(
private fun ScrimSurface(
modifier: Modifier = Modifier,
showScrim: Boolean = true,
- @FloatRange(from = 0.0, to = 1.0) alpha: Float = 0.3f,
+ alpha: Float = 0.3f,
icon: @Composable () -> Unit,
) {
Surface(
diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Image.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Image.kt
new file mode 100644
index 0000000000..5f374ee4ce
--- /dev/null
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Image.kt
@@ -0,0 +1,205 @@
+// Copyright 2022, Google LLC, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package app.tivi.common.compose.ui
+
+import androidx.compose.animation.Crossfade
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Image
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.DefaultAlpha
+import androidx.compose.ui.graphics.FilterQuality
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import com.seiko.imageloader.ImageLoader
+import com.seiko.imageloader.ImageRequestState
+import com.seiko.imageloader.LocalImageLoader
+import com.seiko.imageloader.asImageBitmap
+import com.seiko.imageloader.model.ImageRequest
+import com.seiko.imageloader.model.ImageRequestBuilder
+import com.seiko.imageloader.model.ImageResult
+import com.seiko.imageloader.option.Scale
+import com.seiko.imageloader.option.SizeResolver
+import com.seiko.imageloader.toPainter
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.withContext
+
+@Composable
+fun AsyncImage(
+ model: Any?,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ onState: ((ImageRequestState) -> Unit)? = null,
+ requestBuilder: (ImageRequestBuilder.() -> ImageRequestBuilder)? = null,
+ imageLoader: ImageLoader = LocalImageLoader.current,
+ alignment: Alignment = Alignment.Center,
+ contentScale: ContentScale = ContentScale.Fit,
+ alpha: Float = DefaultAlpha,
+ colorFilter: ColorFilter? = null,
+ filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
+) {
+ val sizeResolver = ConstraintsSizeResolver()
+ var requestState: ImageRequestState by remember { mutableStateOf(ImageRequestState.Loading()) }
+
+ val request by produceState(null, model, contentScale) {
+ value = ImageRequest {
+ data(model)
+ size(sizeResolver)
+ requestBuilder?.invoke(this)
+
+ options {
+ if (scale == Scale.AUTO) {
+ scale = contentScale.toScale()
+ }
+ }
+ eventListener { event ->
+ requestState = ImageRequestState.Loading(event)
+ }
+ }
+ }
+
+ var result by remember { mutableStateOf(null) }
+ LaunchedEffect(imageLoader) {
+ snapshotFlow { request }
+ .filterNotNull()
+ .mapLatest {
+ withContext(imageLoader.config.imageScope.coroutineContext) {
+ imageLoader.execute(it)
+ }
+ }
+ .collect { result = it }
+ }
+
+ val lastOnState by rememberUpdatedState(onState)
+ LaunchedEffect(Unit) {
+ snapshotFlow { requestState }
+ .collect { lastOnState?.invoke(it) }
+ }
+
+ Crossfade(
+ targetState = result,
+ animationSpec = tween(durationMillis = 220),
+ label = "AsyncImage-Crossfade",
+ ) { r ->
+ ResultImage(
+ result = r,
+ alignment = alignment,
+ contentDescription = contentDescription,
+ contentScale = contentScale,
+ alpha = alpha,
+ colorFilter = colorFilter,
+ modifier = modifier.then(sizeResolver),
+ filterQuality = filterQuality,
+ )
+ }
+}
+
+@Composable
+private fun ResultImage(
+ result: ImageResult?,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ alignment: Alignment = Alignment.Center,
+ contentScale: ContentScale = ContentScale.Fit,
+ alpha: Float = DefaultAlpha,
+ colorFilter: ColorFilter? = null,
+ filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
+) {
+ Image(
+ painter = when (result) {
+ is ImageResult.Bitmap -> {
+ BitmapPainter(
+ image = result.bitmap.asImageBitmap(),
+ filterQuality = filterQuality,
+ )
+ }
+
+ is ImageResult.Image -> result.image.toPainter(filterQuality)
+ is ImageResult.Painter -> result.painter
+ is ImageResult.Error -> TODO()
+ is ImageResult.Source -> TODO()
+ null -> EmptyPainter
+ },
+ alignment = alignment,
+ contentDescription = contentDescription,
+ contentScale = contentScale,
+ alpha = alpha,
+ colorFilter = colorFilter,
+ modifier = modifier,
+ )
+}
+
+private object EmptyPainter : Painter() {
+ override val intrinsicSize get() = Size.Unspecified
+ override fun DrawScope.onDraw() = Unit
+}
+
+/** A [SizeResolver] that computes the size from the constrains passed during the layout phase. */
+internal class ConstraintsSizeResolver : SizeResolver, LayoutModifier {
+
+ private val _constraints = MutableStateFlow(Constraints())
+
+ override suspend fun Density.size(): Size {
+ return _constraints.mapNotNull(Constraints::toSizeOrNull).first()
+ }
+
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints,
+ ): MeasureResult {
+ // Cache the current constraints.
+ _constraints.value = constraints
+
+ // Measure and layout the content.
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+
+ fun setConstraints(constraints: Constraints) {
+ _constraints.value = constraints
+ }
+}
+
+@Stable
+private fun Constraints.toSizeOrNull() = when {
+ isZero -> null
+ else -> Size(
+ width = if (hasBoundedWidth) maxWidth.toFloat() else 0f,
+ height = if (hasBoundedHeight) maxHeight.toFloat() else 0f,
+ )
+}
+
+private fun ContentScale.toScale() = when (this) {
+ ContentScale.Fit, ContentScale.Inside -> Scale.FIT
+ else -> Scale.FILL
+}
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/LoadingButton.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/LoadingButton.kt
similarity index 86%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/LoadingButton.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/LoadingButton.kt
index 360fea7a28..cac5b0f1bc 100644
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/LoadingButton.kt
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/LoadingButton.kt
@@ -6,7 +6,6 @@ package app.tivi.common.compose.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
@@ -15,12 +14,10 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ButtonElevation
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import app.tivi.common.compose.Layout
@@ -59,13 +56,3 @@ fun LoadingButton(
content()
}
}
-
-@Preview
-@Composable
-fun PreviewLoadingButton() {
- Column {
- LoadingButton(showProgressIndicator = true, onClick = {}) {
- Text("LoadingButton")
- }
- }
-}
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/PaddingValues.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/PaddingValues.kt
similarity index 100%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/PaddingValues.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/PaddingValues.kt
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/Position.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Position.kt
similarity index 100%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/Position.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/Position.kt
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/PosterCard.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/PosterCard.kt
similarity index 97%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/PosterCard.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/PosterCard.kt
index 6225a10e0c..888a5c24b0 100644
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/PosterCard.kt
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/PosterCard.kt
@@ -55,7 +55,6 @@ private fun PosterCardContent(show: TiviShow) {
)
AsyncImage(
model = show.asImageModel(ImageType.POSTER),
- requestBuilder = { crossfade(true) },
contentDescription = stringResource(
MR.strings.cd_show_poster_image,
show.title ?: "show",
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/RefreshButton.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/RefreshButton.kt
similarity index 100%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/RefreshButton.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/RefreshButton.kt
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/SearchTextField.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/SearchTextField.kt
similarity index 100%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/SearchTextField.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/SearchTextField.kt
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/SortChip.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/SortChip.kt
similarity index 100%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/SortChip.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/SortChip.kt
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/SortMenuPopup.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/SortMenuPopup.kt
similarity index 100%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/SortMenuPopup.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/SortMenuPopup.kt
diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/TimePickerDialog.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/TimePickerDialog.kt
new file mode 100644
index 0000000000..bc28d9172b
--- /dev/null
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/TimePickerDialog.kt
@@ -0,0 +1,41 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.compose.ui
+
+// @OptIn(ExperimentalMaterial3Api::class)
+// @Composable
+// fun TimePickerDialog(
+// onDismissRequest: () -> Unit,
+// confirmButton: @Composable () -> Unit,
+// modifier: Modifier = Modifier,
+// dismissButton: @Composable (() -> Unit)? = null,
+// shape: Shape = MaterialTheme.shapes.extraLarge,
+// tonalElevation: Dp = DatePickerDefaults.TonalElevation,
+// properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false),
+// content: @Composable () -> Unit,
+// ) {
+// AlertDialog(
+// onDismissRequest = onDismissRequest,
+// properties = properties,
+// modifier = modifier,
+// ) {
+// Surface(
+// shape = shape,
+// tonalElevation = tonalElevation,
+// ) {
+// Column {
+// content()
+//
+// Row(
+// modifier = Modifier
+// .align(Alignment.End)
+// .padding(bottom = 8.dp, end = 6.dp),
+// ) {
+// dismissButton?.invoke()
+// confirmButton()
+// }
+// }
+// }
+// }
+// }
diff --git a/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/TiviAlertDialog.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/TiviAlertDialog.kt
new file mode 100644
index 0000000000..6eaa9fda71
--- /dev/null
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/TiviAlertDialog.kt
@@ -0,0 +1,28 @@
+// Copyright 2021, Google LLC, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.compose.ui
+
+import androidx.compose.runtime.Composable
+import com.vanpra.composematerialdialogs.MaterialDialog
+import com.vanpra.composematerialdialogs.message
+import com.vanpra.composematerialdialogs.title
+
+@Composable
+fun TiviAlertDialog(
+ title: String,
+ message: String,
+ confirmText: String,
+ onConfirm: () -> Unit,
+ dismissText: String,
+ onDismissRequest: () -> Unit,
+) {
+ MaterialDialog(
+ onCloseRequest = { onDismissRequest() },
+ ) {
+ title(title)
+ message(message)
+ dialogButtons.positiveButton(text = confirmText, onClick = onConfirm)
+ dialogButtons.negativeButton(text = dismissText, onClick = onDismissRequest)
+ }
+}
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/UserProfileButton.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/UserProfileButton.kt
similarity index 96%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/UserProfileButton.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/UserProfileButton.kt
index 754f313615..61a66e5793 100644
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/UserProfileButton.kt
+++ b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/UserProfileButton.kt
@@ -33,7 +33,6 @@ fun UserProfileButton(
loggedIn && user?.avatarUrl != null -> {
AsyncImage(
model = user.avatarUrl!!,
- requestBuilder = { crossfade(true) },
contentDescription = stringResource(
MR.strings.cd_profile_pic,
user.name ?: user.username,
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/WindowInsets.kt b/common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/WindowInsets.kt
similarity index 100%
rename from common/ui/compose/src/main/java/app/tivi/common/compose/ui/WindowInsets.kt
rename to common/ui/compose/src/commonMain/kotlin/app/tivi/common/compose/ui/WindowInsets.kt
diff --git a/common/ui/compose/src/iosMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt b/common/ui/compose/src/iosMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt
new file mode 100644
index 0000000000..0b4b84e39a
--- /dev/null
+++ b/common/ui/compose/src/iosMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt
@@ -0,0 +1,10 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.compose
+
+import androidx.compose.runtime.Composable
+
+@Composable
+actual fun ReportDrawnWhen(predicate: () -> Boolean) {
+}
diff --git a/common/ui/compose/src/iosMain/kotlin/app/tivi/common/compose/theme/Platform.kt b/common/ui/compose/src/iosMain/kotlin/app/tivi/common/compose/theme/Platform.kt
new file mode 100644
index 0000000000..ae74115409
--- /dev/null
+++ b/common/ui/compose/src/iosMain/kotlin/app/tivi/common/compose/theme/Platform.kt
@@ -0,0 +1,16 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.compose.theme
+
+import androidx.compose.material3.ColorScheme
+import androidx.compose.runtime.Composable
+
+@Composable
+internal actual fun colorScheme(
+ useDarkColors: Boolean,
+ useDynamicColors: Boolean,
+): ColorScheme = when {
+ useDarkColors -> TiviDarkColors
+ else -> TiviLightColors
+}
diff --git a/common/ui/compose/src/jvmMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt b/common/ui/compose/src/jvmMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt
new file mode 100644
index 0000000000..0b4b84e39a
--- /dev/null
+++ b/common/ui/compose/src/jvmMain/kotlin/app/tivi/common/compose/ReportDrawnWhen.kt
@@ -0,0 +1,10 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.compose
+
+import androidx.compose.runtime.Composable
+
+@Composable
+actual fun ReportDrawnWhen(predicate: () -> Boolean) {
+}
diff --git a/common/ui/compose/src/jvmMain/kotlin/app/tivi/common/compose/theme/Platform.kt b/common/ui/compose/src/jvmMain/kotlin/app/tivi/common/compose/theme/Platform.kt
new file mode 100644
index 0000000000..ae74115409
--- /dev/null
+++ b/common/ui/compose/src/jvmMain/kotlin/app/tivi/common/compose/theme/Platform.kt
@@ -0,0 +1,16 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.common.compose.theme
+
+import androidx.compose.material3.ColorScheme
+import androidx.compose.runtime.Composable
+
+@Composable
+internal actual fun colorScheme(
+ useDarkColors: Boolean,
+ useDynamicColors: Boolean,
+): ColorScheme = when {
+ useDarkColors -> TiviDarkColors
+ else -> TiviLightColors
+}
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/Debug.kt b/common/ui/compose/src/main/java/app/tivi/common/compose/Debug.kt
deleted file mode 100644
index d6dc547428..0000000000
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/Debug.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2020, Google LLC, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-@file:Suppress("NOTHING_TO_INLINE")
-
-package app.tivi.common.compose
-
-import android.util.Log
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.remember
-
-class Ref(var value: Int)
-
-const val EnableDebugCompositionLogs = false
-
-/**
- * An effect which logs the number compositions at the invoked point of the slot table.
- * Thanks to [objcode](https://github.com/objcode) for this code.
- *
- * This is an inline function to act as like a C-style #include to the host composable function.
- * That way we track it's compositions, not this function's compositions.
- *
- * @param tag Log tag used for [Log.d]
- */
-@Composable
-inline fun LogCompositions(tag: String) {
- if (EnableDebugCompositionLogs && BuildConfig.DEBUG) {
- val ref = remember { Ref(0) }
- SideEffect { ref.value++ }
- Log.d(tag, "Compositions: ${ref.value}")
- }
-}
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/WindowSizeClass.kt b/common/ui/compose/src/main/java/app/tivi/common/compose/WindowSizeClass.kt
deleted file mode 100644
index e156ba4338..0000000000
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/WindowSizeClass.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright 2023, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package app.tivi.common.compose
-
-import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
-import androidx.compose.material3.windowsizeclass.WindowSizeClass
-import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.compose.ui.unit.DpSize
-
-@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
-val LocalWindowSizeClass = staticCompositionLocalOf {
- WindowSizeClass.calculateFromSize(DpSize.Zero)
-}
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/theme/Theme.kt b/common/ui/compose/src/main/java/app/tivi/common/compose/theme/Theme.kt
deleted file mode 100644
index 87e34c5add..0000000000
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/theme/Theme.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright 2020, Google LLC, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package app.tivi.common.compose.theme
-
-import android.os.Build
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.dynamicDarkColorScheme
-import androidx.compose.material3.dynamicLightColorScheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.platform.LocalContext
-
-@Composable
-fun TiviTheme(
- useDarkColors: Boolean = isSystemInDarkTheme(),
- useDynamicColors: Boolean = false,
- content: @Composable () -> Unit,
-) {
- MaterialTheme(
- colorScheme = when {
- Build.VERSION.SDK_INT >= 31 && useDynamicColors && useDarkColors -> {
- dynamicDarkColorScheme(LocalContext.current)
- }
- Build.VERSION.SDK_INT >= 31 && useDynamicColors && !useDarkColors -> {
- dynamicLightColorScheme(LocalContext.current)
- }
- useDarkColors -> TiviDarkColors
- else -> TiviLightColors
- },
- typography = TiviTypography,
- shapes = TiviShapes,
- content = content,
- )
-}
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/AndroidDialog.kt b/common/ui/compose/src/main/java/app/tivi/common/compose/ui/AndroidDialog.kt
deleted file mode 100644
index 7d23ce46bf..0000000000
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/AndroidDialog.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright 2023, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package app.tivi.common.compose.ui
-
-import android.content.res.Configuration
-import androidx.compose.foundation.layout.widthIn
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.unit.dp
-import kotlin.math.roundToInt
-
-/**
- * Implements a suitable min-width for Android dialog content.
- *
- * Matches the values used in the platform default dialog themes `Theme.Material.Dialog.MinWidth`
- * and `Theme.Material.Dialog.Alert`. Unfortunately the necessary attributes used in the themes
- * are private, so we can't read them from the theme (and AppCompat duplicates them too).
- *
- * The values in question can be found here:
- * https://cs.android.com/search?q=dialog_min_width%20file:dimens.xml&sq=&ss=android%2Fplatform%2Fsuperproject:frameworks%2Fbase%2F
- *
- * This primarily exists to workaround https://issuetracker.google.com/issues/221643630, which
- * requires the workaround of using `DialogProperties(usePlatformDefaultWidth = false)`.
- *
- * @param clampMaxWidth Whether to clamp the maximum width to the same value. This is useful for
- * Compose content as fillMaxWidth() (or similar) is frequently used, which then stretches the
- * dialog to fill the screen width.
- */
-fun Modifier.androidMinWidthDialogSize(
- clampMaxWidth: Boolean = false,
-): Modifier = composed {
- val configuration = LocalConfiguration.current
- val density = LocalContext.current.resources.displayMetrics.density
-
- val displayWidth = (configuration.screenWidthDp * density).roundToInt()
- val displayHeight = (configuration.screenHeightDp * density).roundToInt()
-
- val minWidthRatio: Float = when {
- configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_XLARGE) -> {
- if (displayWidth > displayHeight) 0.45f else 0.72f
- }
- configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE) -> {
- if (displayWidth > displayHeight) 0.55f else 0.8f
- }
- else -> {
- if (displayWidth > displayHeight) 0.65f else 0.95f
- }
- }
-
- if (clampMaxWidth) {
- Modifier.widthIn(max = ((displayWidth * minWidthRatio) / density).dp)
- } else {
- Modifier.widthIn(min = ((displayWidth * minWidthRatio) / density).dp)
- }
-}
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/Image.kt b/common/ui/compose/src/main/java/app/tivi/common/compose/ui/Image.kt
deleted file mode 100644
index 4e820df439..0000000000
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/Image.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2022, Google LLC, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package app.tivi.common.compose.ui
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.graphics.DefaultAlpha
-import androidx.compose.ui.graphics.FilterQuality
-import androidx.compose.ui.graphics.drawscope.DrawScope
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalContext
-import coil.compose.AsyncImagePainter
-import coil.request.ImageRequest
-
-@Composable
-fun AsyncImage(
- model: Any?,
- contentDescription: String?,
- modifier: Modifier = Modifier,
- transform: (AsyncImagePainter.State) -> AsyncImagePainter.State = AsyncImagePainter.DefaultTransform,
- onState: ((AsyncImagePainter.State) -> Unit)? = null,
- requestBuilder: (ImageRequest.Builder.() -> ImageRequest.Builder)? = null,
- alignment: Alignment = Alignment.Center,
- contentScale: ContentScale = ContentScale.Fit,
- alpha: Float = DefaultAlpha,
- colorFilter: ColorFilter? = null,
- filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
-) {
- coil.compose.AsyncImage(
- model = requestBuilder?.let { builder ->
- when (model) {
- is ImageRequest -> model.newBuilder()
- else -> ImageRequest.Builder(LocalContext.current).data(model)
- }.apply { this.builder() }.build()
- } ?: model,
- contentDescription = contentDescription,
- modifier = modifier,
- transform = transform,
- onState = onState,
- alignment = alignment,
- contentScale = contentScale,
- alpha = alpha,
- colorFilter = colorFilter,
- filterQuality = filterQuality,
- )
-}
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/TimePickerDialog.kt b/common/ui/compose/src/main/java/app/tivi/common/compose/ui/TimePickerDialog.kt
deleted file mode 100644
index 85d46bb59a..0000000000
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/TimePickerDialog.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright 2023, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package app.tivi.common.compose.ui
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.DatePickerDefaults
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.DialogProperties
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun TimePickerDialog(
- onDismissRequest: () -> Unit,
- confirmButton: @Composable () -> Unit,
- modifier: Modifier = Modifier,
- dismissButton: @Composable (() -> Unit)? = null,
- shape: Shape = MaterialTheme.shapes.extraLarge,
- tonalElevation: Dp = DatePickerDefaults.TonalElevation,
- properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false),
- content: @Composable () -> Unit,
-) {
- AlertDialog(
- onDismissRequest = onDismissRequest,
- properties = properties,
- modifier = modifier,
- ) {
- Surface(
- shape = shape,
- tonalElevation = tonalElevation,
- ) {
- Column {
- content()
-
- Row(
- modifier = Modifier
- .align(Alignment.End)
- .padding(bottom = 8.dp, end = 6.dp),
- ) {
- dismissButton?.invoke()
- confirmButton()
- }
- }
- }
- }
-}
diff --git a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/TiviAlertDialog.kt b/common/ui/compose/src/main/java/app/tivi/common/compose/ui/TiviAlertDialog.kt
deleted file mode 100644
index f2169763d2..0000000000
--- a/common/ui/compose/src/main/java/app/tivi/common/compose/ui/TiviAlertDialog.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright 2021, Google LLC, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package app.tivi.common.compose.ui
-
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.OutlinedButton
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-
-@Composable
-fun TiviAlertDialog(
- title: String,
- message: String,
- confirmText: String,
- onConfirm: () -> Unit,
- dismissText: String,
- onDismissRequest: () -> Unit,
-) {
- AlertDialog(
- title = { Text(text = title) },
- text = { Text(text = message) },
- confirmButton = {
- OutlinedButton(onClick = { onConfirm() }) {
- Text(text = confirmText)
- }
- },
- dismissButton = {
- TextButton(onClick = { onDismissRequest() }) {
- Text(text = dismissText)
- }
- },
- onDismissRequest = onDismissRequest,
- )
-}
diff --git a/common/ui/resources-compose/build.gradle.kts b/common/ui/resources-compose/build.gradle.kts
deleted file mode 100644
index 3dbbf13986..0000000000
--- a/common/ui/resources-compose/build.gradle.kts
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright 2023, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-
-plugins {
- id("app.tivi.android.library")
- id("app.tivi.android.compose")
- id("app.tivi.kotlin.android")
-}
-
-android {
- namespace = "dev.icerock.moko.resources.compose"
-
- buildFeatures {
- buildConfig = true
- }
-}
-
-dependencies {
- api(platform(libs.compose.bom))
- implementation(libs.compose.foundation.foundation)
-
- api(libs.moko.resources)
-}
diff --git a/common/ui/resources-compose/src/main/AndroidManifest.xml b/common/ui/resources-compose/src/main/AndroidManifest.xml
deleted file mode 100644
index 03649fd118..0000000000
--- a/common/ui/resources-compose/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/AssetResource.kt b/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/AssetResource.kt
deleted file mode 100644
index 850639585e..0000000000
--- a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/AssetResource.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright 2023, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package dev.icerock.moko.resources.compose
-
-import android.content.Context
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.produceState
-import androidx.compose.ui.platform.LocalContext
-import dev.icerock.moko.resources.AssetResource
-
-@Composable
-fun AssetResource.readTextAsState(): State {
- val context: Context = LocalContext.current
- return produceState(null, this, context) {
- value = readText(context)
- }
-}
diff --git a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/ColorResource.kt b/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/ColorResource.kt
deleted file mode 100644
index 8008c033a0..0000000000
--- a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/ColorResource.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright 2023, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package dev.icerock.moko.resources.compose
-
-import android.content.Context
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-import dev.icerock.moko.resources.ColorResource
-
-@Composable
-fun colorResource(resource: ColorResource): Color {
- val context: Context = LocalContext.current
- return Color(resource.getColor(context))
-}
diff --git a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/FileResource.kt b/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/FileResource.kt
deleted file mode 100644
index f9452b59a5..0000000000
--- a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/FileResource.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright 2023, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package dev.icerock.moko.resources.compose
-
-import android.content.Context
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.produceState
-import androidx.compose.ui.platform.LocalContext
-import dev.icerock.moko.resources.FileResource
-
-@Composable
-fun FileResource.readTextAsState(): State {
- val context: Context = LocalContext.current
- return produceState(null, this, context) {
- value = readText(context)
- }
-}
diff --git a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/FontResource.kt b/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/FontResource.kt
deleted file mode 100644
index b74f114068..0000000000
--- a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/FontResource.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright 2023, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package dev.icerock.moko.resources.compose
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.text.font.Font
-import androidx.compose.ui.text.font.FontStyle
-import androidx.compose.ui.text.font.FontWeight
-import dev.icerock.moko.resources.FontResource
-
-@Suppress("RedundantNullableReturnType")
-@Composable
-fun FontResource.asFont(
- weight: FontWeight = FontWeight.Normal,
- style: FontStyle = FontStyle.Normal,
-): Font? = remember(fontResourceId) {
- Font(
- resId = fontResourceId,
- weight = weight,
- style = style,
- )
-}
diff --git a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/ImageResource.kt b/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/ImageResource.kt
deleted file mode 100644
index 0c39bad7ff..0000000000
--- a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/ImageResource.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright 2023, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package dev.icerock.moko.resources.compose
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.painter.Painter
-import dev.icerock.moko.resources.ImageResource
-
-@Composable
-fun painterResource(imageResource: ImageResource): Painter {
- return androidx.compose.ui.res.painterResource(id = imageResource.drawableResId)
-}
diff --git a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/StringDescExt.kt b/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/StringDescExt.kt
deleted file mode 100644
index 9f9728a6ba..0000000000
--- a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/StringDescExt.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright 2022, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package dev.icerock.moko.resources.compose
-
-import android.content.Context
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.platform.LocalContext
-import dev.icerock.moko.resources.desc.StringDesc
-
-@Composable
-fun StringDesc.localized(): String {
- val context: Context = LocalContext.current
- return toString(context)
-}
diff --git a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/StringResource.kt b/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/StringResource.kt
deleted file mode 100644
index 3695c5be74..0000000000
--- a/common/ui/resources-compose/src/main/kotlin/dev/icerock/moko/resources/compose/StringResource.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright 2021, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package dev.icerock.moko.resources.compose
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.platform.LocalContext
-import dev.icerock.moko.resources.PluralsResource
-import dev.icerock.moko.resources.StringResource
-import dev.icerock.moko.resources.desc.Plural
-import dev.icerock.moko.resources.desc.PluralFormatted
-import dev.icerock.moko.resources.desc.Resource
-import dev.icerock.moko.resources.desc.ResourceFormatted
-import dev.icerock.moko.resources.desc.StringDesc
-
-@Composable
-fun stringResource(resource: StringResource): String =
- StringDesc.Resource(resource).toString(LocalContext.current)
-
-@Composable
-fun stringResource(resource: StringResource, vararg args: Any): String =
- StringDesc.ResourceFormatted(resource, *args).toString(LocalContext.current)
-
-@Composable
-fun stringResource(resource: PluralsResource, quantity: Int): String =
- StringDesc.Plural(resource, quantity).toString(LocalContext.current)
-
-@Composable
-fun stringResource(resource: PluralsResource, quantity: Int, vararg args: Any): String =
- StringDesc.PluralFormatted(resource, quantity, *args).toString(LocalContext.current)
diff --git a/common/ui/resources/src/appleMain/kotlin/app/tivi/util/TiviDateFormatter.kt b/common/ui/resources/src/appleMain/kotlin/app/tivi/util/TiviDateFormatter.kt
index d58997f096..c814468f61 100644
--- a/common/ui/resources/src/appleMain/kotlin/app/tivi/util/TiviDateFormatter.kt
+++ b/common/ui/resources/src/appleMain/kotlin/app/tivi/util/TiviDateFormatter.kt
@@ -3,6 +3,7 @@
package app.tivi.util
+import app.tivi.inject.ActivityScope
import kotlin.time.Duration.Companion.days
import kotlinx.cinterop.convert
import kotlinx.datetime.Instant
@@ -10,6 +11,7 @@ import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalTime
import kotlinx.datetime.toNSDate
import kotlinx.datetime.toNSDateComponents
+import me.tatarka.inject.annotations.Inject
import platform.Foundation.NSCalendar
import platform.Foundation.NSCalendar.Companion.currentCalendar
import platform.Foundation.NSDate
@@ -23,6 +25,8 @@ import platform.Foundation.NSRelativeDateTimeFormatter
import platform.Foundation.NSRelativeDateTimeFormatterStyleNamed
import platform.Foundation.autoupdatingCurrentLocale
+@ActivityScope
+@Inject
actual class TiviDateFormatter(
locale: NSLocale = NSLocale.autoupdatingCurrentLocale,
) {
diff --git a/common/ui/resources/src/appleMain/kotlin/app/tivi/util/TiviTextCreator.kt b/common/ui/resources/src/appleMain/kotlin/app/tivi/util/TiviTextCreator.kt
index 151fe353d6..c924d28594 100644
--- a/common/ui/resources/src/appleMain/kotlin/app/tivi/util/TiviTextCreator.kt
+++ b/common/ui/resources/src/appleMain/kotlin/app/tivi/util/TiviTextCreator.kt
@@ -5,6 +5,7 @@ package app.tivi.util
import app.tivi.common.ui.resources.MR
import app.tivi.data.models.TiviShow
+import app.tivi.inject.ActivityScope
import dev.icerock.moko.resources.desc.StringDesc
import dev.icerock.moko.resources.format
import kotlinx.cinterop.convert
@@ -14,6 +15,7 @@ import kotlinx.datetime.isoDayNumber
import kotlinx.datetime.toKotlinInstant
import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.toNSTimeZone
+import me.tatarka.inject.annotations.Inject
import platform.Foundation.NSCalendar
import platform.Foundation.NSCalendarUnitHour
import platform.Foundation.NSCalendarUnitMinute
@@ -22,6 +24,8 @@ import platform.Foundation.NSCalendarUnitWeekday
import platform.Foundation.NSDate
import platform.Foundation.NSDateComponentsFormatter
+@ActivityScope
+@Inject
actual class TiviTextCreator(
override val dateFormatter: TiviDateFormatter,
) : CommonTiviTextCreator {
diff --git a/core/base/src/commonMain/kotlin/app/tivi/appinitializers/AppInitializer.kt b/core/base/src/commonMain/kotlin/app/tivi/appinitializers/AppInitializer.kt
index 4879393e09..80c09a867f 100644
--- a/core/base/src/commonMain/kotlin/app/tivi/appinitializers/AppInitializer.kt
+++ b/core/base/src/commonMain/kotlin/app/tivi/appinitializers/AppInitializer.kt
@@ -4,5 +4,5 @@
package app.tivi.appinitializers
fun interface AppInitializer {
- fun init()
+ fun initialize()
}
diff --git a/core/logging/src/commonMain/kotlin/app/tivi/util/LoggerInitializer.kt b/core/logging/src/commonMain/kotlin/app/tivi/util/LoggerInitializer.kt
index 8ab51bd7dc..b11551010f 100644
--- a/core/logging/src/commonMain/kotlin/app/tivi/util/LoggerInitializer.kt
+++ b/core/logging/src/commonMain/kotlin/app/tivi/util/LoggerInitializer.kt
@@ -13,7 +13,7 @@ class LoggerInitializer(
private val logger: Logger,
private val applicationInfo: ApplicationInfo,
) : AppInitializer {
- override fun init() {
+ override fun initialize() {
logger.setup(
debugMode = when {
applicationInfo.debugBuild -> true
diff --git a/core/powercontroller/src/iosMain/kotlin/app/tivi/util/PowerControllerComponent.kt b/core/powercontroller/src/iosMain/kotlin/app/tivi/util/PowerControllerComponent.kt
index c2ecbf5048..6aaf8b029e 100644
--- a/core/powercontroller/src/iosMain/kotlin/app/tivi/util/PowerControllerComponent.kt
+++ b/core/powercontroller/src/iosMain/kotlin/app/tivi/util/PowerControllerComponent.kt
@@ -7,5 +7,5 @@ import me.tatarka.inject.annotations.Provides
actual interface PowerControllerComponent {
@Provides
- fun providePowerController() = EmptyPowerController
+ fun providePowerController(): PowerController = EmptyPowerController
}
diff --git a/core/powercontroller/src/jvmMain/kotlin/app/tivi/util/PowerControllerComponent.kt b/core/powercontroller/src/jvmMain/kotlin/app/tivi/util/PowerControllerComponent.kt
index c2ecbf5048..6aaf8b029e 100644
--- a/core/powercontroller/src/jvmMain/kotlin/app/tivi/util/PowerControllerComponent.kt
+++ b/core/powercontroller/src/jvmMain/kotlin/app/tivi/util/PowerControllerComponent.kt
@@ -7,5 +7,5 @@ import me.tatarka.inject.annotations.Provides
actual interface PowerControllerComponent {
@Provides
- fun providePowerController() = EmptyPowerController
+ fun providePowerController(): PowerController = EmptyPowerController
}
diff --git a/core/preferences/src/commonMain/kotlin/app/tivi/settings/PreferencesInitializer.kt b/core/preferences/src/commonMain/kotlin/app/tivi/settings/PreferencesInitializer.kt
index 289bb556d4..7b032361b5 100644
--- a/core/preferences/src/commonMain/kotlin/app/tivi/settings/PreferencesInitializer.kt
+++ b/core/preferences/src/commonMain/kotlin/app/tivi/settings/PreferencesInitializer.kt
@@ -10,7 +10,7 @@ import me.tatarka.inject.annotations.Inject
class PreferencesInitializer(
private val prefs: TiviPreferences,
) : AppInitializer {
- override fun init() {
+ override fun initialize() {
prefs.setup()
}
}
diff --git a/core/preferences/src/commonMain/kotlin/app/tivi/settings/TiviPreferences.kt b/core/preferences/src/commonMain/kotlin/app/tivi/settings/TiviPreferences.kt
index 8e9a79e3e4..5ea77e57f5 100644
--- a/core/preferences/src/commonMain/kotlin/app/tivi/settings/TiviPreferences.kt
+++ b/core/preferences/src/commonMain/kotlin/app/tivi/settings/TiviPreferences.kt
@@ -40,9 +40,7 @@ object EmptyTiviPreferences : TiviPreferences {
override var theme: TiviPreferences.Theme = TiviPreferences.Theme.SYSTEM
- override fun observeTheme(): Flow {
- TODO("Not yet implemented")
- }
+ override fun observeTheme(): Flow = emptyFlow()
override var useDynamicColors: Boolean = false
diff --git a/data/test/src/commonTest/kotlin/app/tivi/data/DatabaseTest.kt b/data/test/src/commonTest/kotlin/app/tivi/data/DatabaseTest.kt
index 566ff1ee6b..61b0a128e6 100644
--- a/data/test/src/commonTest/kotlin/app/tivi/data/DatabaseTest.kt
+++ b/data/test/src/commonTest/kotlin/app/tivi/data/DatabaseTest.kt
@@ -6,6 +6,7 @@ package app.tivi.data
import app.cash.sqldelight.db.SqlDriver
import app.moviebase.tmdb.Tmdb3
import app.moviebase.trakt.Trakt
+import app.tivi.data.traktauth.AuthState
import app.tivi.data.traktauth.RefreshTraktTokensInteractor
import app.tivi.data.traktauth.TraktAuthState
import app.tivi.inject.ApplicationScope
@@ -39,7 +40,9 @@ abstract class TestApplicationComponent :
@Provides
fun provideRefreshTraktTokensInteractor(): RefreshTraktTokensInteractor {
- return RefreshTraktTokensInteractor { null }
+ return object : RefreshTraktTokensInteractor {
+ override suspend fun invoke(): AuthState? = null
+ }
}
@Provides
diff --git a/data/traktauth/src/androidMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt b/data/traktauth/src/androidMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt
index 081c9eac6b..7329aa9345 100644
--- a/data/traktauth/src/androidMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt
+++ b/data/traktauth/src/androidMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt
@@ -53,7 +53,7 @@ actual interface TraktAuthComponent {
@ApplicationScope
@Provides
- fun provideAuthStore(manager: TiviAuthStore): AuthStore = manager
+ fun provideAuthStore(store: TiviAuthStore): AuthStore = store
}
interface TraktAuthActivityComponent {
diff --git a/data/traktauth/src/commonMain/kotlin/app/tivi/data/traktauth/RefreshTraktTokensInteractor.kt b/data/traktauth/src/commonMain/kotlin/app/tivi/data/traktauth/RefreshTraktTokensInteractor.kt
index b836b231fb..a7511b6bb1 100644
--- a/data/traktauth/src/commonMain/kotlin/app/tivi/data/traktauth/RefreshTraktTokensInteractor.kt
+++ b/data/traktauth/src/commonMain/kotlin/app/tivi/data/traktauth/RefreshTraktTokensInteractor.kt
@@ -3,6 +3,6 @@
package app.tivi.data.traktauth
-fun interface RefreshTraktTokensInteractor {
+interface RefreshTraktTokensInteractor {
suspend operator fun invoke(): AuthState?
}
diff --git a/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosAuthStore.kt b/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosAuthStore.kt
new file mode 100644
index 0000000000..2dc2fbba75
--- /dev/null
+++ b/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosAuthStore.kt
@@ -0,0 +1,23 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.data.traktauth
+
+import app.tivi.data.traktauth.store.AuthStore
+import me.tatarka.inject.annotations.Inject
+
+@Inject
+class IosAuthStore : AuthStore {
+ override suspend fun get(): AuthState? {
+ // TODO no-op for now
+ return null
+ }
+
+ override suspend fun save(state: AuthState) {
+ // TODO no-op for now
+ }
+
+ override suspend fun clear() {
+ // TODO no-op for now
+ }
+}
diff --git a/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosLoginToTraktInteractor.kt b/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosLoginToTraktInteractor.kt
new file mode 100644
index 0000000000..4b7056542e
--- /dev/null
+++ b/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosLoginToTraktInteractor.kt
@@ -0,0 +1,17 @@
+// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.data.traktauth
+
+import me.tatarka.inject.annotations.Inject
+
+@Inject
+class IosLoginToTraktInteractor : LoginToTraktInteractor {
+ override fun register() {
+ // TODO
+ }
+
+ override fun launch() {
+ // TODO
+ }
+}
diff --git a/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosRefreshTraktTokensInteractor.kt b/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosRefreshTraktTokensInteractor.kt
new file mode 100644
index 0000000000..c3542755ae
--- /dev/null
+++ b/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/IosRefreshTraktTokensInteractor.kt
@@ -0,0 +1,14 @@
+// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.data.traktauth
+
+import me.tatarka.inject.annotations.Inject
+
+@Inject
+class IosRefreshTraktTokensInteractor : RefreshTraktTokensInteractor {
+ override suspend fun invoke(): AuthState? {
+ // TODO
+ return null
+ }
+}
diff --git a/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt b/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt
index e7b355aabe..99b1fc874e 100644
--- a/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt
+++ b/data/traktauth/src/iosMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt
@@ -3,4 +3,20 @@
package app.tivi.data.traktauth
-actual interface TraktAuthComponent
+import app.tivi.data.traktauth.store.AuthStore
+import app.tivi.inject.ApplicationScope
+import me.tatarka.inject.annotations.Provides
+
+actual interface TraktAuthComponent {
+ @ApplicationScope
+ @Provides
+ fun provideAuthStore(store: IosAuthStore): AuthStore = store
+
+ @ApplicationScope
+ @Provides
+ fun provideRefreshTraktTokensInteractor(impl: IosRefreshTraktTokensInteractor): RefreshTraktTokensInteractor = impl
+
+ @Provides
+ @ApplicationScope
+ fun provideLoginToTraktInteractor(impl: IosLoginToTraktInteractor): LoginToTraktInteractor = impl
+}
diff --git a/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopAuthStore.kt b/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopAuthStore.kt
new file mode 100644
index 0000000000..04ce99cb05
--- /dev/null
+++ b/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopAuthStore.kt
@@ -0,0 +1,23 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.data.traktauth
+
+import app.tivi.data.traktauth.store.AuthStore
+import me.tatarka.inject.annotations.Inject
+
+@Inject
+class DesktopAuthStore : AuthStore {
+ override suspend fun get(): AuthState? {
+ // TODO no-op for now
+ return null
+ }
+
+ override suspend fun save(state: AuthState) {
+ // TODO no-op for now
+ }
+
+ override suspend fun clear() {
+ // TODO no-op for now
+ }
+}
diff --git a/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopLoginToTraktInteractor.kt b/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopLoginToTraktInteractor.kt
new file mode 100644
index 0000000000..8cc3421e63
--- /dev/null
+++ b/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopLoginToTraktInteractor.kt
@@ -0,0 +1,17 @@
+// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.data.traktauth
+
+import me.tatarka.inject.annotations.Inject
+
+@Inject
+class DesktopLoginToTraktInteractor : LoginToTraktInteractor {
+ override fun register() {
+ // TODO
+ }
+
+ override fun launch() {
+ // TODO
+ }
+}
diff --git a/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopRefreshTraktTokensInteractor.kt b/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopRefreshTraktTokensInteractor.kt
new file mode 100644
index 0000000000..3d6776a120
--- /dev/null
+++ b/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/DesktopRefreshTraktTokensInteractor.kt
@@ -0,0 +1,14 @@
+// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.data.traktauth
+
+import me.tatarka.inject.annotations.Inject
+
+@Inject
+class DesktopRefreshTraktTokensInteractor : RefreshTraktTokensInteractor {
+ override suspend fun invoke(): AuthState? {
+ // TODO
+ return null
+ }
+}
diff --git a/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt b/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt
index e7b355aabe..f2928e69b0 100644
--- a/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt
+++ b/data/traktauth/src/jvmMain/kotlin/app/tivi/data/traktauth/TraktAuthComponent.kt
@@ -3,4 +3,20 @@
package app.tivi.data.traktauth
-actual interface TraktAuthComponent
+import app.tivi.data.traktauth.store.AuthStore
+import app.tivi.inject.ApplicationScope
+import me.tatarka.inject.annotations.Provides
+
+actual interface TraktAuthComponent {
+ @ApplicationScope
+ @Provides
+ fun provideAuthStore(store: DesktopAuthStore): AuthStore = store
+
+ @ApplicationScope
+ @Provides
+ fun provideRefreshTraktTokensInteractor(impl: DesktopRefreshTraktTokensInteractor): RefreshTraktTokensInteractor = impl
+
+ @Provides
+ @ApplicationScope
+ fun provideLoginToTraktInteractor(impl: DesktopLoginToTraktInteractor): LoginToTraktInteractor = impl
+}
diff --git a/desktop-app/build.gradle.kts b/desktop-app/build.gradle.kts
new file mode 100644
index 0000000000..a9fb3c7823
--- /dev/null
+++ b/desktop-app/build.gradle.kts
@@ -0,0 +1,34 @@
+import org.jetbrains.compose.desktop.application.dsl.TargetFormat
+
+// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+plugins {
+ // We have to use KMP due to Moko-resources
+ // https://github.com/icerockdev/moko-resources/issues/263
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
+}
+
+kotlin {
+ sourceSets {
+ val jvmMain by getting {
+ dependencies {
+ implementation(projects.shared)
+ implementation(compose.desktop.currentOs)
+ }
+ }
+ }
+}
+
+compose.desktop {
+ application {
+ mainClass = "app.tivi.MainKt"
+
+ nativeDistributions {
+ targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
+ packageName = "app.tivi"
+ packageVersion = "1.0.0"
+ }
+ }
+}
diff --git a/desktop-app/src/jvmMain/kotlin/app/tivi/Main.kt b/desktop-app/src/jvmMain/kotlin/app/tivi/Main.kt
new file mode 100644
index 0000000000..d978e438ed
--- /dev/null
+++ b/desktop-app/src/jvmMain/kotlin/app/tivi/Main.kt
@@ -0,0 +1,42 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi
+
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.window.Window
+import androidx.compose.ui.window.application
+import app.tivi.inject.DesktopApplicationComponent
+import app.tivi.inject.WindowComponent
+import app.tivi.inject.create
+
+fun main() = application {
+ val applicationComponent = remember {
+ DesktopApplicationComponent.create()
+ }
+
+ LaunchedEffect(applicationComponent) {
+ applicationComponent.initializers.initialize()
+ }
+
+ Window(
+ title = "Tivi",
+ onCloseRequest = ::exitApplication,
+ ) {
+ val component = remember(applicationComponent) {
+ WindowComponent.create(applicationComponent)
+ }
+
+ component.tiviContent(
+ onRootPop = {
+ // TODO
+ },
+ onOpenSettings = {
+ // TODO
+ },
+ modifier = Modifier,
+ )
+ }
+}
diff --git a/gradle.properties b/gradle.properties
index a3477a1dd2..a4d8b5d84a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -35,3 +35,8 @@ android.defaults.buildFeatures.buildConfig=false
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.mpp.androidGradlePluginCompatibility.nowarn=true
+
+# https://github.com/JetBrains/compose-multiplatform/issues/2046
+kotlin.native.cacheKind=none
+
+org.jetbrains.compose.experimental.uikit.enabled=true
diff --git a/gradle/build-logic/convention/build.gradle.kts b/gradle/build-logic/convention/build.gradle.kts
index a6b7a36d7d..f08607204b 100644
--- a/gradle/build-logic/convention/build.gradle.kts
+++ b/gradle/build-logic/convention/build.gradle.kts
@@ -49,10 +49,5 @@ gradlePlugin {
id = "app.tivi.android.test"
implementationClass = "app.tivi.gradle.AndroidTestConventionPlugin"
}
-
- register("androidCompose") {
- id = "app.tivi.android.compose"
- implementationClass = "app.tivi.gradle.AndroidComposeConventionPlugin"
- }
}
}
diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidCompose.kt b/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidCompose.kt
deleted file mode 100644
index 8ffa3d96e4..0000000000
--- a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidCompose.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright 2023, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package app.tivi.gradle
-
-import com.android.build.api.dsl.CommonExtension
-import org.gradle.api.Project
-import org.gradle.kotlin.dsl.configure
-
-fun Project.configureAndroidCompose() {
- android {
- buildFeatures {
- compose = true
- }
-
- composeOptions {
- kotlinCompilerExtensionVersion = libs.findVersion("composecompiler").get().toString()
- }
- }
-}
-
-private fun Project.android(action: CommonExtension<*, *, *, *>.() -> Unit) =
- extensions.configure(CommonExtension::class, action)
diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidComposeConventionPlugin.kt b/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidComposeConventionPlugin.kt
deleted file mode 100644
index 41990f5826..0000000000
--- a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidComposeConventionPlugin.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright 2023, Christopher Banes and the Tivi project contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package app.tivi.gradle
-
-import org.gradle.api.Plugin
-import org.gradle.api.Project
-
-class AndroidComposeConventionPlugin : Plugin {
- override fun apply(target: Project) = with(target) {
- pluginManager.withPlugin("com.android.base") {
- configureAndroidCompose()
- }
- }
-}
diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/Android.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Android.kt
similarity index 100%
rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/Android.kt
rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Android.kt
diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidApplicationConventionPlugin.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/AndroidApplicationConventionPlugin.kt
similarity index 100%
rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidApplicationConventionPlugin.kt
rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/AndroidApplicationConventionPlugin.kt
diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidApplicationLauncher.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/AndroidApplicationLauncher.kt
similarity index 100%
rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidApplicationLauncher.kt
rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/AndroidApplicationLauncher.kt
diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidLibraryConventionPlugin.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/AndroidLibraryConventionPlugin.kt
similarity index 100%
rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidLibraryConventionPlugin.kt
rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/AndroidLibraryConventionPlugin.kt
diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidTestConventionPlugin.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/AndroidTestConventionPlugin.kt
similarity index 100%
rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/AndroidTestConventionPlugin.kt
rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/AndroidTestConventionPlugin.kt
diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/Java.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Java.kt
similarity index 100%
rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/Java.kt
rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Java.kt
diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/Kotlin.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Kotlin.kt
similarity index 100%
rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/Kotlin.kt
rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Kotlin.kt
diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/KotlinAndroidConventionPlugin.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/KotlinAndroidConventionPlugin.kt
similarity index 100%
rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/KotlinAndroidConventionPlugin.kt
rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/KotlinAndroidConventionPlugin.kt
diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/KotlinMultiplatformConventionPlugin.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/KotlinMultiplatformConventionPlugin.kt
similarity index 100%
rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/KotlinMultiplatformConventionPlugin.kt
rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/KotlinMultiplatformConventionPlugin.kt
diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/RootConventionPlugin.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/RootConventionPlugin.kt
similarity index 100%
rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/RootConventionPlugin.kt
rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/RootConventionPlugin.kt
diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/Spotless.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Spotless.kt
similarity index 92%
rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/Spotless.kt
rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Spotless.kt
index faa9a00a2d..c1056aebad 100644
--- a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/Spotless.kt
+++ b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Spotless.kt
@@ -8,6 +8,11 @@ import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
fun Project.configureSpotless() {
+ if (path.startsWith(":thirdparty")) {
+ println("Skipping Spotless")
+ return
+ }
+
val ktlintVersion = libs.findVersion("ktlint").get().requiredVersion
with(pluginManager) {
@@ -16,7 +21,7 @@ fun Project.configureSpotless() {
spotless {
kotlin {
- target("**/*.kt")
+ target("src/**/*.kt")
targetExclude("$buildDir/**/*.kt")
targetExclude("bin/**/*.kt")
ktlint(ktlintVersion)
@@ -32,7 +37,7 @@ fun Project.configureSpotless() {
}
kotlinGradle {
- target("**/*.kts")
+ target("src/**/*.kts")
targetExclude("$buildDir/**/*.kts")
ktlint(ktlintVersion)
licenseHeaderFile(rootProject.file("spotless/google-copyright.txt"), "(^(?![\\/ ]\\**).*$)")
diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/VersionCatalog.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/VersionCatalog.kt
similarity index 100%
rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/VersionCatalog.kt
rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/VersionCatalog.kt
diff --git a/gradle/build-logic/convention/src/main/java/app/tivi/gradle/Versions.kt b/gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Versions.kt
similarity index 100%
rename from gradle/build-logic/convention/src/main/java/app/tivi/gradle/Versions.kt
rename to gradle/build-logic/convention/src/main/kotlin/app/tivi/gradle/Versions.kt
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9aac88b4b0..1f42b0e609 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -4,12 +4,9 @@ androidxlifecycle = "2.6.1"
androidxactivity = "1.7.2"
chucker = "4.0.0"
circuit = "0.10.0"
-coil = "2.4.0"
-compose-bom = "2023.03.00"
-composecompiler = "1.4.7"
coroutines = "1.7.2"
debugdrawer = "0.9.8"
-kotlin = "1.8.21"
+kotlin = "1.8.20"
kotlininject = "0.6.1"
ktlint = "0.49.1"
moko-resources = "0.23.0"
@@ -28,9 +25,10 @@ android-lint = { id = "com.android.lint", version.ref = "agp" }
android-test = { id = "com.android.test", version.ref = "agp" }
buildConfig = "com.github.gmazzo.buildconfig:4.1.1"
cacheFixPlugin = { id = "org.gradle.android.cache-fix", version = "2.7.2" }
+composeMultiplatform = { id = "org.jetbrains.compose", version = "1.4.1" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
-ksp = "com.google.devtools.ksp:1.8.21-1.0.11"
+ksp = "com.google.devtools.ksp:1.8.20-1.0.11"
moko-resources = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko-resources" }
gms-googleServices = "com.google.gms.google-services:4.3.15"
firebase-crashlytics = "com.google.firebase.crashlytics:2.9.6"
@@ -73,38 +71,24 @@ circuit-foundation = { module = "com.slack.circuit:circuit-foundation", version.
circuit-overlay = { module = "com.slack.circuit:circuit-overlay", version.ref = "circuit" }
circuit-runtime = { module = "com.slack.circuit:circuit-runtime", version.ref = "circuit" }
-coil-coil = { module = "io.coil-kt:coil", version.ref = "coil" }
-coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
-coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" }
+compose-material3-windowsizeclass = "dev.chrisbanes.material3:material3-window-size-class-multiplatform:0.2.0"
-tools-desugarjdklibs = "com.android.tools:desugar_jdk_libs:2.0.3"
+materialdialogs-core = "ca.gosyer:compose-material-dialogs-core:0.9.3"
-compose-bom = { module = "dev.chrisbanes.compose:compose-bom", version.ref = "compose-bom" }
-compose-animation-animation = { module = "androidx.compose.animation:animation" }
-compose-foundation-foundation = { module = "androidx.compose.foundation:foundation" }
-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" }
-compose-material-iconsext = { module = "androidx.compose.material:material-icons-extended" }
-compose-material-material = { module = "androidx.compose.material:material" }
-compose-material3-material3 = { module = "androidx.compose.material3:material3" }
-compose-material3-windowsizeclass = { module = "androidx.compose.material3:material3-window-size-class" }
-compose-ui-test = { module = "androidx.compose.ui:ui-test-junit4" }
-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
-compose-ui-ui = { module = "androidx.compose.ui:ui" }
-compose-ui-uitextfonts = { module = "androidx.compose.ui:ui-text-google-fonts" }
-compose-ui-util = { module = "androidx.compose.ui:ui-util" }
-compose-ui-viewbinding = { module = "androidx.compose.ui:ui-viewbinding" }
-
-# This isn't strictly used, but allows Renovate to see us using the Compose Compiler artifact
-compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "composecompiler" }
+tools-desugarjdklibs = "com.android.tools:desugar_jdk_libs:2.0.3"
google-firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.3.0"
google-firebase-crashlytics = "com.google.firebase:firebase-crashlytics-ktx:18.3.7"
google-firebase-perf = "com.google.firebase:firebase-perf-ktx:20.3.3"
+insetsx = "com.moriatsushi.insetsx:insetsx:0.1.0-alpha10"
+
debugdrawer-debugdrawer = { module = "au.com.gridstone.debugdrawer:debugdrawer", version.ref = "debugdrawer" }
debugdrawer-timber = { module = "au.com.gridstone.debugdrawer:debugdrawer-timber", version.ref = "debugdrawer" }
debugdrawer-okhttplogger = { module = "au.com.gridstone.debugdrawer:debugdrawer-okhttp-logger", version.ref = "debugdrawer" }
+imageloader = "io.github.qdsfdhvh:image-loader:1.5.1"
+
junit = "junit:junit:4.13.2"
kermit-kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
@@ -124,9 +108,10 @@ kotlininject-runtime = { module = "me.tatarka.inject:kotlin-inject-runtime", ver
# This isn't strictly used, but allows Renovate to see us using the ktlint artifact
ktlint = { module = "com.pinterest:ktlint", version.ref = "ktlint" }
-leakCanary = "com.squareup.leakcanary:leakcanary-android:2.12"
+leakCanary = "com.squareup.leakcanary:leakcanary-android:2.11"
moko-resources = { module = "dev.icerock.moko:resources", version.ref = "moko-resources" }
+moko-resourcesCompose = { module = "dev.icerock.moko:resources-compose", version.ref = "moko-resources" }
okhttp-loggingInterceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
okhttp-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
@@ -140,8 +125,6 @@ playservices-blockstore = "com.google.android.gms:play-services-auth-blockstore:
robolectric = "org.robolectric:robolectric:4.10.3"
-swipe = "me.saket.swipe:swipe:1.2.0"
-
store = "org.mobilenativefoundation.store:store5:5.0.0-beta01"
sqldelight-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
@@ -160,6 +143,8 @@ assertk = "com.willowtreeapps.assertk:assertk:0.26.1"
turbine = "app.cash.turbine:turbine:1.0.0"
+uuid = "com.benasher44:uuid:0.7.1"
+
# Build logic dependencies
android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
diff --git a/ios-app/Tivi/Tivi.xcodeproj/project.pbxproj b/ios-app/Tivi/Tivi.xcodeproj/project.pbxproj
new file mode 100644
index 0000000000..73f657cf97
--- /dev/null
+++ b/ios-app/Tivi/Tivi.xcodeproj/project.pbxproj
@@ -0,0 +1,401 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 833349382A4CCCEE00F464FE /* TiviApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833349372A4CCCEE00F464FE /* TiviApp.swift */; };
+ 8333493A2A4CCCEE00F464FE /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833349392A4CCCEE00F464FE /* ContentView.swift */; };
+ 8333493C2A4CCCEF00F464FE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8333493B2A4CCCEF00F464FE /* Assets.xcassets */; };
+ 8333493F2A4CCCEF00F464FE /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8333493E2A4CCCEF00F464FE /* Preview Assets.xcassets */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 38282FFD2A4F318E00E7929E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
+ 833349342A4CCCEE00F464FE /* Tivi.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Tivi.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 833349372A4CCCEE00F464FE /* TiviApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TiviApp.swift; sourceTree = ""; };
+ 833349392A4CCCEE00F464FE /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
+ 8333493B2A4CCCEF00F464FE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 8333493E2A4CCCEF00F464FE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 833349312A4CCCEE00F464FE /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 8333492B2A4CCCEE00F464FE = {
+ isa = PBXGroup;
+ children = (
+ 833349362A4CCCEE00F464FE /* Tivi */,
+ 833349352A4CCCEE00F464FE /* Products */,
+ );
+ sourceTree = "";
+ };
+ 833349352A4CCCEE00F464FE /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 833349342A4CCCEE00F464FE /* Tivi.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 833349362A4CCCEE00F464FE /* Tivi */ = {
+ isa = PBXGroup;
+ children = (
+ 38282FFD2A4F318E00E7929E /* Info.plist */,
+ 833349372A4CCCEE00F464FE /* TiviApp.swift */,
+ 833349392A4CCCEE00F464FE /* ContentView.swift */,
+ 8333493B2A4CCCEF00F464FE /* Assets.xcassets */,
+ 8333493D2A4CCCEF00F464FE /* Preview Content */,
+ );
+ path = Tivi;
+ sourceTree = "";
+ };
+ 8333493D2A4CCCEF00F464FE /* Preview Content */ = {
+ isa = PBXGroup;
+ children = (
+ 8333493E2A4CCCEF00F464FE /* Preview Assets.xcassets */,
+ );
+ path = "Preview Content";
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 833349332A4CCCEE00F464FE /* Tivi */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 833349422A4CCCEF00F464FE /* Build configuration list for PBXNativeTarget "Tivi" */;
+ buildPhases = (
+ 833349452A4CCD9F00F464FE /* ShellScript */,
+ 833349302A4CCCEE00F464FE /* Sources */,
+ 833349312A4CCCEE00F464FE /* Frameworks */,
+ 833349322A4CCCEE00F464FE /* Resources */,
+ 38282FFA2A4F242200E7929E /* ShellScript */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Tivi;
+ productName = Tivi;
+ productReference = 833349342A4CCCEE00F464FE /* Tivi.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 8333492C2A4CCCEE00F464FE /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1430;
+ LastUpgradeCheck = 1430;
+ TargetAttributes = {
+ 833349332A4CCCEE00F464FE = {
+ CreatedOnToolsVersion = 14.3.1;
+ };
+ };
+ };
+ buildConfigurationList = 8333492F2A4CCCEE00F464FE /* Build configuration list for PBXProject "Tivi" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 8333492B2A4CCCEE00F464FE;
+ productRefGroup = 833349352A4CCCEE00F464FE /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 833349332A4CCCEE00F464FE /* Tivi */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 833349322A4CCCEE00F464FE /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8333493F2A4CCCEF00F464FE /* Preview Assets.xcassets in Resources */,
+ 8333493C2A4CCCEF00F464FE /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 38282FFA2A4F242200E7929E /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "cd \"$SRCROOT/../..\"\n./gradlew :shared:copyFrameworkResourcesToApp \\\n -Pmoko.resources.PLATFORM_NAME=\"$PLATFORM_NAME\" \\\n -Pmoko.resources.CONFIGURATION=\"$CONFIGURATION\" \\\n -Pmoko.resources.ARCHS=\"$ARCHS\" \\\n -Pmoko.resources.BUILT_PRODUCTS_DIR=\"$BUILT_PRODUCTS_DIR\" \\\n -Pmoko.resources.CONTENTS_FOLDER_PATH=\"$CONTENTS_FOLDER_PATH\" \n";
+ };
+ 833349452A4CCD9F00F464FE /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "cd \"$SRCROOT/../..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\n";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 833349302A4CCCEE00F464FE /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8333493A2A4CCCEE00F464FE /* ContentView.swift in Sources */,
+ 833349382A4CCCEE00F464FE /* TiviApp.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 833349402A4CCCEF00F464FE /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.4;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 833349412A4CCCEF00F464FE /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.4;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 833349432A4CCCEF00F464FE /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"Tivi/Preview Content\"";
+ ENABLE_PREVIEWS = YES;
+ FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)";
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = Tivi/Info.plist;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ "-framework",
+ TiviKt,
+ "-lsqlite3",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = app.tivi;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 833349442A4CCCEF00F464FE /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"Tivi/Preview Content\"";
+ ENABLE_PREVIEWS = YES;
+ FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)";
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = Tivi/Info.plist;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ "-framework",
+ TiviKt,
+ "-lsqlite3",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = app.tivi;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 8333492F2A4CCCEE00F464FE /* Build configuration list for PBXProject "Tivi" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 833349402A4CCCEF00F464FE /* Debug */,
+ 833349412A4CCCEF00F464FE /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 833349422A4CCCEF00F464FE /* Build configuration list for PBXNativeTarget "Tivi" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 833349432A4CCCEF00F464FE /* Debug */,
+ 833349442A4CCCEF00F464FE /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 8333492C2A4CCCEE00F464FE /* Project object */;
+}
diff --git a/ios-app/Tivi/Tivi.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios-app/Tivi/Tivi.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000000..919434a625
--- /dev/null
+++ b/ios-app/Tivi/Tivi.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/ios-app/Tivi/Tivi.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios-app/Tivi/Tivi.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/ios-app/Tivi/Tivi.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ios-app/Tivi/Tivi/Assets.xcassets/AccentColor.colorset/Contents.json b/ios-app/Tivi/Tivi/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000000..eb87897008
--- /dev/null
+++ b/ios-app/Tivi/Tivi/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios-app/Tivi/Tivi/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios-app/Tivi/Tivi/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000000..13613e3ee1
--- /dev/null
+++ b/ios-app/Tivi/Tivi/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,13 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios-app/Tivi/Tivi/Assets.xcassets/Contents.json b/ios-app/Tivi/Tivi/Assets.xcassets/Contents.json
new file mode 100644
index 0000000000..73c00596a7
--- /dev/null
+++ b/ios-app/Tivi/Tivi/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios-app/Tivi/Tivi/ContentView.swift b/ios-app/Tivi/Tivi/ContentView.swift
new file mode 100644
index 0000000000..1a28ee3573
--- /dev/null
+++ b/ios-app/Tivi/Tivi/ContentView.swift
@@ -0,0 +1,40 @@
+//
+// ContentView.swift
+// Tivi
+//
+// Created by Chris Banes on 28/06/2023.
+//
+
+import SwiftUI
+import TiviKt
+
+struct ContentView: View {
+ let component: HomeUiControllerComponent
+
+ init(component: HomeUiControllerComponent) {
+ self.component = component
+ }
+
+ var body: some View {
+ ComposeView(component: self.component)
+ .ignoresSafeArea(.all, edges: .bottom) // Compose has own keyboard handler
+ }
+}
+
+struct ComposeView: UIViewControllerRepresentable {
+ let component: HomeUiControllerComponent
+
+ init(component: HomeUiControllerComponent) {
+ self.component = component
+ }
+
+ func makeUIViewController(context: Context) -> UIViewController {
+ component.uiViewController {
+ // onRootPop
+ } onOpenSettings: {
+ // no-op
+ }
+ }
+
+ func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
+}
diff --git a/ios-app/Tivi/Tivi/Info.plist b/ios-app/Tivi/Tivi/Info.plist
new file mode 100644
index 0000000000..c2e0e36112
--- /dev/null
+++ b/ios-app/Tivi/Tivi/Info.plist
@@ -0,0 +1,7 @@
+
+
+
+
+ CADisableMinimumFrameDurationOnPhone
+
+
diff --git a/ios-app/Tivi/Tivi/Preview Content/Preview Assets.xcassets/Contents.json b/ios-app/Tivi/Tivi/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000000..73c00596a7
--- /dev/null
+++ b/ios-app/Tivi/Tivi/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios-app/Tivi/Tivi/TiviApp.swift b/ios-app/Tivi/Tivi/TiviApp.swift
new file mode 100644
index 0000000000..c802513c29
--- /dev/null
+++ b/ios-app/Tivi/Tivi/TiviApp.swift
@@ -0,0 +1,27 @@
+//
+// TiviApp.swift
+// Tivi
+//
+// Created by Chris Banes on 28/06/2023.
+//
+
+import SwiftUI
+import TiviKt
+
+@main
+struct TiviApp: App {
+ let applicationComponent = IosApplicationComponent.companion.create()
+
+ init() {
+ applicationComponent.initializers.initialize()
+ }
+
+ var body: some Scene {
+ WindowGroup {
+ let uiComponent = HomeUiControllerComponent.companion.create(
+ applicationComponent: applicationComponent
+ )
+ ContentView(component: uiComponent)
+ }
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 0d3775d269..96d705b1ff 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -38,13 +38,12 @@ gradleEnterprise {
buildScan {
termsOfServiceUrl = "https://gradle.com/terms-of-service"
termsOfServiceAgree = "yes"
- publishAlways()
}
}
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
// https://docs.gradle.org/7.6/userguide/configuration_cache.html#config_cache:stable
-enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
+// enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
rootProject.name = "tivi"
@@ -57,7 +56,6 @@ include(
":core:preferences",
":common:ui:circuit-overlay",
":common:ui:resources",
- ":common:ui:resources-compose",
":common:ui:compose",
":common:ui:screens",
":common:imageloading",
@@ -96,7 +94,10 @@ include(
":ui:library",
":ui:account",
":ui:upnext",
+ ":ui:root",
":android-app:app",
":android-app:benchmark",
":android-app:common-test",
+ ":desktop-app",
+ ":thirdparty:swipe",
)
diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts
index 360a4ce163..c925722c15 100644
--- a/shared/build.gradle.kts
+++ b/shared/build.gradle.kts
@@ -1,13 +1,28 @@
// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors
// SPDX-License-Identifier: Apache-2.0
+import app.tivi.gradle.addKspDependencyForAllTargets
+import org.jetbrains.kotlin.gradle.plugin.mpp.Framework
+import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
id("app.tivi.android.library")
id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
+ alias(libs.plugins.ksp)
+ alias(libs.plugins.moko.resources)
}
kotlin {
+ targets.withType {
+ binaries.withType {
+ isStatic = true
+ baseName = "TiviKt"
+
+ export(projects.ui.root)
+ }
+ }
+
sourceSets {
val commonMain by getting {
dependencies {
@@ -22,13 +37,30 @@ kotlin {
api(projects.api.tmdb)
api(projects.domain)
api(projects.tasks)
+
+ api(projects.common.imageloading)
+ api(projects.common.ui.compose)
+
+ api(projects.ui.account)
+ api(projects.ui.discover)
+ api(projects.ui.episode.details)
+ api(projects.ui.episode.track)
+ api(projects.ui.library)
+ api(projects.ui.popular)
+ api(projects.ui.trending)
+ api(projects.ui.recommended)
+ api(projects.ui.search)
+ api(projects.ui.show.details)
+ api(projects.ui.show.seasons)
+ api(projects.ui.root)
+ // api(projects.ui.settings)
+ api(projects.ui.upnext)
}
}
- val androidMain by getting {
+ val jvmMain by getting {
dependencies {
- api(projects.common.imageloading)
- api(projects.common.ui.compose)
+ api(libs.okhttp.okhttp)
}
}
}
@@ -37,3 +69,36 @@ kotlin {
android {
namespace = "app.tivi.shared"
}
+
+ksp {
+ arg("me.tatarka.inject.generateCompanionExtensions", "true")
+}
+
+addKspDependencyForAllTargets(libs.kotlininject.compiler)
+
+multiplatformResources {
+ disableStaticFrameworkWarning = true
+ multiplatformResourcesPackage = "app.tivi"
+ multiplatformResourcesSourceSet = "iosMain"
+}
+
+// Various fixes for moko-resources tasks
+// iOS
+afterEvaluate {
+ tasks.findByPath("kspKotlinIosArm64")?.apply {
+ dependsOn(tasks.getByPath("generateMRiosArm64Main"))
+ }
+ tasks.findByPath("kspKotlinIosSimulatorArm64")?.apply {
+ dependsOn(tasks.getByPath("generateMRiosSimulatorArm64Main"))
+ }
+ tasks.findByPath("kspKotlinIosX64")?.apply {
+ dependsOn(tasks.getByPath("generateMRiosX64Main"))
+ }
+}
+// Android
+tasks.withType(com.android.build.gradle.tasks.MergeResources::class).configureEach {
+ dependsOn(tasks.getByPath("generateMRandroidMain"))
+}
+tasks.withType(com.android.build.gradle.tasks.MapSourceSetPathsTask::class).configureEach {
+ dependsOn(tasks.getByPath("generateMRandroidMain"))
+}
diff --git a/shared/src/commonMain/kotlin/app/tivi/appinitializers/AppInitializers.kt b/shared/src/commonMain/kotlin/app/tivi/appinitializers/AppInitializers.kt
index d804614b34..50106d07e7 100644
--- a/shared/src/commonMain/kotlin/app/tivi/appinitializers/AppInitializers.kt
+++ b/shared/src/commonMain/kotlin/app/tivi/appinitializers/AppInitializers.kt
@@ -10,11 +10,11 @@ import me.tatarka.inject.annotations.Inject
class AppInitializers(
private val initializers: Set,
private val tracer: Tracer,
-) {
- fun init() {
+) : AppInitializer {
+ override fun initialize() {
tracer.trace("AppInitializers") {
for (initializer in initializers) {
- initializer.init()
+ initializer.initialize()
}
}
}
diff --git a/shared/src/commonMain/kotlin/app/tivi/appinitializers/TmdbInitializer.kt b/shared/src/commonMain/kotlin/app/tivi/appinitializers/TmdbInitializer.kt
index ebd15ea18c..84fa30ceda 100644
--- a/shared/src/commonMain/kotlin/app/tivi/appinitializers/TmdbInitializer.kt
+++ b/shared/src/commonMain/kotlin/app/tivi/appinitializers/TmdbInitializer.kt
@@ -16,7 +16,7 @@ class TmdbInitializer(
private val updateTmdbConfig: UpdateTmdbConfig,
private val dispatchers: AppCoroutineDispatchers,
) : AppInitializer {
- override fun init() {
+ override fun initialize() {
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(dispatchers.main) {
updateTmdbConfig.invoke()
diff --git a/shared/src/commonMain/kotlin/app/tivi/inject/SharedApplicationComponent.kt b/shared/src/commonMain/kotlin/app/tivi/inject/SharedApplicationComponent.kt
index 5b9906aeb5..57820f5dcf 100644
--- a/shared/src/commonMain/kotlin/app/tivi/inject/SharedApplicationComponent.kt
+++ b/shared/src/commonMain/kotlin/app/tivi/inject/SharedApplicationComponent.kt
@@ -5,6 +5,7 @@ package app.tivi.inject
import app.tivi.appinitializers.AppInitializer
import app.tivi.appinitializers.TmdbInitializer
+import app.tivi.common.imageloading.ImageLoadingComponent
import app.tivi.core.analytics.AnalyticsComponent
import app.tivi.core.perf.PerformanceComponent
import app.tivi.data.SqlDelightDatabaseComponent
@@ -36,7 +37,8 @@ interface SharedApplicationComponent :
ApiComponent,
TasksComponent,
CoreComponent,
- DataComponent
+ DataComponent,
+ ImageLoadingComponent
interface ApiComponent : TmdbComponent, TraktComponent
diff --git a/android-app/app/src/main/java/app/tivi/inject/UiComponent.kt b/shared/src/commonMain/kotlin/app/tivi/inject/UiComponent.kt
similarity index 87%
rename from android-app/app/src/main/java/app/tivi/inject/UiComponent.kt
rename to shared/src/commonMain/kotlin/app/tivi/inject/UiComponent.kt
index d908935516..b779152868 100644
--- a/android-app/app/src/main/java/app/tivi/inject/UiComponent.kt
+++ b/shared/src/commonMain/kotlin/app/tivi/inject/UiComponent.kt
@@ -33,15 +33,14 @@ interface UiComponent :
ShowSeasonsComponent,
TrendingShowsComponent,
UpNextComponent {
+
@Provides
- @ApplicationScope
+ @ActivityScope
fun provideCircuitConfig(
uiFactories: Set,
presenterFactories: Set,
- ): CircuitConfig {
- return CircuitConfig.Builder()
- .addUiFactories(uiFactories)
- .addPresenterFactories(presenterFactories)
- .build()
- }
+ ): CircuitConfig = CircuitConfig.Builder()
+ .addUiFactories(uiFactories)
+ .addPresenterFactories(presenterFactories)
+ .build()
}
diff --git a/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt b/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt
new file mode 100644
index 0000000000..3e4d1a29a3
--- /dev/null
+++ b/shared/src/iosMain/kotlin/app/tivi/inject/HomeUiControllerComponent.kt
@@ -0,0 +1,26 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.inject
+
+import app.tivi.home.TiviUiViewController
+import me.tatarka.inject.annotations.Component
+import platform.UIKit.UIViewController
+
+@ActivityScope
+@Component
+abstract class HomeUiControllerComponent(
+ @Component val applicationComponent: IosApplicationComponent,
+) : UiComponent {
+ abstract val viewController: TiviUiViewController
+
+ /**
+ * Function which makes [viewController] easier to call from Swift
+ */
+ fun uiViewController(
+ onRootPop: () -> Unit,
+ onOpenSettings: () -> Unit,
+ ): UIViewController = viewController(onRootPop, onOpenSettings)
+
+ companion object
+}
diff --git a/shared/src/iosMain/kotlin/app/tivi/inject/IosApplicationComponent.kt b/shared/src/iosMain/kotlin/app/tivi/inject/IosApplicationComponent.kt
new file mode 100644
index 0000000000..97318e03c9
--- /dev/null
+++ b/shared/src/iosMain/kotlin/app/tivi/inject/IosApplicationComponent.kt
@@ -0,0 +1,31 @@
+// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.inject
+
+import androidx.compose.ui.unit.Density
+import app.tivi.app.ApplicationInfo
+import app.tivi.app.Flavor
+import app.tivi.appinitializers.AppInitializers
+import me.tatarka.inject.annotations.Component
+import me.tatarka.inject.annotations.Provides
+import platform.Foundation.NSBundle
+
+@Component
+@ApplicationScope
+abstract class IosApplicationComponent : SharedApplicationComponent {
+ abstract val initializers: AppInitializers
+
+ @ApplicationScope
+ @Provides
+ fun provideApplicationId(): ApplicationInfo = ApplicationInfo(
+ packageName = NSBundle.mainBundle.bundleIdentifier ?: "empty.bundle.id",
+ debugBuild = Platform.isDebugBinary,
+ flavor = Flavor.Standard,
+ )
+
+ @Provides
+ fun provideDensity(): Density = Density(density = 1f) // FIXME
+
+ companion object
+}
diff --git a/shared/src/jvmMain/kotlin/app/tivi/inject/DesktopApplicationComponent.kt b/shared/src/jvmMain/kotlin/app/tivi/inject/DesktopApplicationComponent.kt
new file mode 100644
index 0000000000..52686f32d6
--- /dev/null
+++ b/shared/src/jvmMain/kotlin/app/tivi/inject/DesktopApplicationComponent.kt
@@ -0,0 +1,55 @@
+// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.inject
+
+import androidx.compose.ui.unit.Density
+import app.tivi.app.ApplicationInfo
+import app.tivi.app.Flavor
+import app.tivi.appinitializers.AppInitializers
+import java.util.concurrent.TimeUnit
+import me.tatarka.inject.annotations.Component
+import me.tatarka.inject.annotations.Provides
+import okhttp3.ConnectionPool
+import okhttp3.Dispatcher
+import okhttp3.OkHttpClient
+
+@Component
+@ApplicationScope
+abstract class DesktopApplicationComponent : SharedApplicationComponent {
+ abstract val initializers: AppInitializers
+
+ @ApplicationScope
+ @Provides
+ fun provideApplicationId(): ApplicationInfo = ApplicationInfo(
+ packageName = "app.tivi",
+ debugBuild = true,
+ flavor = Flavor.Standard,
+ )
+
+ @Provides
+ fun provideDensity(): Density = Density(density = 1f) // FIXME
+
+ @ApplicationScope
+ @Provides
+ fun provideOkHttpClient(
+ // interceptors: Set,
+ ): OkHttpClient = OkHttpClient.Builder()
+ // .apply { interceptors.forEach(::addInterceptor) }
+ // Adjust the Connection pool to account for historical use of 3 separate clients
+ // but reduce the keepAlive to 2 minutes to avoid keeping radio open.
+ .connectionPool(ConnectionPool(10, 2, TimeUnit.MINUTES))
+ .dispatcher(
+ Dispatcher().apply {
+ // Allow for increased number of concurrent image fetches on same host
+ maxRequestsPerHost = 10
+ },
+ )
+ // Increase timeouts
+ .connectTimeout(20, TimeUnit.SECONDS)
+ .readTimeout(20, TimeUnit.SECONDS)
+ .writeTimeout(20, TimeUnit.SECONDS)
+ .build()
+
+ companion object
+}
diff --git a/shared/src/jvmMain/kotlin/app/tivi/inject/WindowComponent.kt b/shared/src/jvmMain/kotlin/app/tivi/inject/WindowComponent.kt
new file mode 100644
index 0000000000..93efdf53e3
--- /dev/null
+++ b/shared/src/jvmMain/kotlin/app/tivi/inject/WindowComponent.kt
@@ -0,0 +1,17 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.inject
+
+import app.tivi.home.TiviContent
+import me.tatarka.inject.annotations.Component
+
+@ActivityScope
+@Component
+abstract class WindowComponent(
+ @Component val applicationComponent: DesktopApplicationComponent,
+) : UiComponent {
+ abstract val tiviContent: TiviContent
+
+ companion object
+}
diff --git a/tasks/src/commonMain/kotlin/app/tivi/tasks/ShowTasksInitializer.kt b/tasks/src/commonMain/kotlin/app/tivi/tasks/ShowTasksInitializer.kt
index 179400e9fa..1c6bd156f0 100644
--- a/tasks/src/commonMain/kotlin/app/tivi/tasks/ShowTasksInitializer.kt
+++ b/tasks/src/commonMain/kotlin/app/tivi/tasks/ShowTasksInitializer.kt
@@ -10,7 +10,7 @@ import me.tatarka.inject.annotations.Inject
class ShowTasksInitializer(
private val showTasks: Lazy,
) : AppInitializer {
- override fun init() {
+ override fun initialize() {
showTasks.value.setupNightSyncs()
}
}
diff --git a/thirdparty/swipe/build.gradle.kts b/thirdparty/swipe/build.gradle.kts
new file mode 100644
index 0000000000..78d44f5d42
--- /dev/null
+++ b/thirdparty/swipe/build.gradle.kts
@@ -0,0 +1,23 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+
+plugins {
+ id("app.tivi.android.library")
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
+}
+
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(compose.foundation)
+ }
+ }
+ }
+}
+
+android {
+ namespace = "me.saket.swipe"
+}
diff --git a/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/ActionFinder.kt b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/ActionFinder.kt
new file mode 100644
index 0000000000..758536050b
--- /dev/null
+++ b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/ActionFinder.kt
@@ -0,0 +1,58 @@
+package me.saket.swipe
+
+import kotlin.math.abs
+
+internal data class SwipeActionMeta(
+ val value: SwipeAction,
+ val isOnRightSide: Boolean,
+)
+
+internal data class ActionFinder(
+ private val left: List,
+ private val right: List
+) {
+
+ fun actionAt(offset: Float, totalWidth: Int): SwipeActionMeta? {
+ if (offset == 0f) {
+ return null
+ }
+
+ val isOnRightSide = offset < 0f
+ val actions = if (isOnRightSide) right else left
+
+ val actionAtOffset = actions.actionAt(
+ offset = abs(offset).coerceAtMost(totalWidth.toFloat()),
+ totalWidth = totalWidth
+ )
+ return actionAtOffset?.let {
+ SwipeActionMeta(
+ value = actionAtOffset,
+ isOnRightSide = isOnRightSide
+ )
+ }
+ }
+
+ private fun List.actionAt(offset: Float, totalWidth: Int): SwipeAction? {
+ if (isEmpty()) {
+ return null
+ }
+
+ val totalWeights = this.sumOf { it.weight }
+ var offsetSoFar = 0.0
+
+ @Suppress("ReplaceManualRangeWithIndicesCalls") // Avoid allocating an Iterator for every pixel swiped.
+ for (i in 0 until size) {
+ val action = this[i]
+ val actionWidth = (action.weight / totalWeights) * totalWidth
+ val actionEndX = offsetSoFar + actionWidth
+
+ if (offset <= actionEndX) {
+ return action
+ }
+ offsetSoFar += actionEndX
+ }
+
+ // Precision error in the above loop maybe?
+ error("Couldn't find any swipe action. Width=$totalWidth, offset=$offset, actions=$this")
+ }
+}
diff --git a/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeAction.kt b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeAction.kt
new file mode 100644
index 0000000000..5fc3986669
--- /dev/null
+++ b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeAction.kt
@@ -0,0 +1,77 @@
+package me.saket.swipe
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.unit.dp
+
+/**
+ * Represents an action that can be shown in [SwipeableActionsBox].
+ *
+ * @param background Color used as the background of [SwipeableActionsBox] while
+ * this action is visible. If this action is swiped, its background color is
+ * also used for drawing a ripple over the content for providing a visual
+ * feedback to the user.
+ *
+ * @param weight The proportional width to give to this element, as related
+ * to the total of all weighted siblings. [SwipeableActionsBox] will divide its
+ * horizontal space and distribute it to actions according to their weight.
+ *
+ * @param isUndo Determines the direction in which a ripple is drawn when this
+ * action is swiped. When false, the ripple grows from this action's position
+ * to consume the entire composable, and vice versa. This can be used for
+ * actions that can be toggled on and off.
+ */
+class SwipeAction(
+ val onSwipe: () -> Unit,
+ val icon: @Composable () -> Unit,
+ val background: Color,
+ val weight: Double = 1.0,
+ val isUndo: Boolean = false
+) {
+ init {
+ require(weight > 0.0) { "invalid weight $weight; must be greater than zero" }
+ }
+
+ fun copy(
+ onSwipe: () -> Unit = this.onSwipe,
+ icon: @Composable () -> Unit = this.icon,
+ background: Color = this.background,
+ weight: Double = this.weight,
+ isUndo: Boolean = this.isUndo,
+ ) = SwipeAction(
+ onSwipe = onSwipe,
+ icon = icon,
+ background = background,
+ weight = weight,
+ isUndo = isUndo
+ )
+}
+
+/**
+ * See [SwipeAction] for documentation.
+ */
+fun SwipeAction(
+ onSwipe: () -> Unit,
+ icon: Painter,
+ background: Color,
+ weight: Double = 1.0,
+ isUndo: Boolean = false
+): SwipeAction {
+ return SwipeAction(
+ icon = {
+ Image(
+ modifier = Modifier.padding(16.dp),
+ painter = icon,
+ contentDescription = null
+ )
+ },
+ background = background,
+ weight = weight,
+ onSwipe = onSwipe,
+ isUndo = isUndo
+ )
+}
diff --git a/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeRipple.kt b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeRipple.kt
new file mode 100644
index 0000000000..b93e62c6b9
--- /dev/null
+++ b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeRipple.kt
@@ -0,0 +1,91 @@
+@file:Suppress("NAME_SHADOWING")
+
+package me.saket.swipe
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.clipRect
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlin.math.roundToInt
+
+@Stable
+internal class SwipeRippleState {
+ private var ripple = mutableStateOf(null)
+
+ suspend fun animate(
+ action: SwipeActionMeta,
+ ) {
+ val drawOnRightSide = action.isOnRightSide
+ val action = action.value
+
+ ripple.value = SwipeRipple(
+ isUndo = action.isUndo,
+ rightSide = drawOnRightSide,
+ color = action.background,
+ alpha = 0f,
+ progress = 0f
+ )
+
+ // Reverse animation feels faster (especially for larger swipe distances) so slow it down further.
+ val animationDurationMs = (animationDurationMs * (if (action.isUndo) 1.75f else 1f)).roundToInt()
+
+ coroutineScope {
+ launch {
+ Animatable(initialValue = 0f).animateTo(
+ targetValue = 1f,
+ animationSpec = tween(durationMillis = animationDurationMs),
+ block = {
+ ripple.value = ripple.value!!.copy(progress = value)
+ }
+ )
+ }
+ launch {
+ Animatable(initialValue = if (action.isUndo) 0f else 0.25f).animateTo(
+ targetValue = if (action.isUndo) 0.5f else 0f,
+ animationSpec = tween(
+ durationMillis = animationDurationMs,
+ delayMillis = if (action.isUndo) 0 else animationDurationMs / 2
+ ),
+ block = {
+ ripple.value = ripple.value!!.copy(alpha = value)
+ }
+ )
+ }
+ }
+ }
+
+ fun draw(scope: DrawScope) {
+ ripple.value?.run {
+ scope.clipRect {
+ val size = scope.size
+ // Start the ripple with a radius equal to the available height so that it covers the entire edge.
+ val startRadius = if (isUndo) size.width + size.height else size.height
+ val endRadius = if (!isUndo) size.width + size.height else size.height
+ val radius = lerp(startRadius, endRadius, fraction = progress)
+
+ drawCircle(
+ color = color,
+ radius = radius,
+ alpha = alpha,
+ center = this.center.copy(x = if (rightSide) this.size.width + this.size.height else 0f - this.size.height)
+ )
+ }
+ }
+ }
+}
+
+private data class SwipeRipple(
+ val isUndo: Boolean,
+ val rightSide: Boolean,
+ val color: Color,
+ val alpha: Float,
+ val progress: Float,
+)
+
+private fun lerp(start: Float, stop: Float, fraction: Float) =
+ (start * (1 - fraction) + stop * fraction)
diff --git a/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeableActionsBox.kt b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeableActionsBox.kt
new file mode 100644
index 0000000000..91a5f2e0e5
--- /dev/null
+++ b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeableActionsBox.kt
@@ -0,0 +1,162 @@
+@file:Suppress("NAME_SHADOWING")
+
+package me.saket.swipe
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.Orientation.Horizontal
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.absoluteOffset
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
+import kotlin.math.abs
+import kotlin.math.roundToInt
+
+/**
+ * A composable that can be swiped left or right for revealing actions.
+ *
+ * @param swipeThreshold Minimum drag distance before any [SwipeAction] is
+ * activated and can be swiped.
+ *
+ * @param backgroundUntilSwipeThreshold Color drawn behind the content until
+ * [swipeThreshold] is reached. When the threshold is passed, this color is
+ * replaced by the currently visible [SwipeAction]'s background.
+ */
+@Composable
+fun SwipeableActionsBox(
+ modifier: Modifier = Modifier,
+ state: SwipeableActionsState = rememberSwipeableActionsState(),
+ startActions: List = emptyList(),
+ endActions: List = emptyList(),
+ swipeThreshold: Dp = 40.dp,
+ backgroundUntilSwipeThreshold: Color = Color.DarkGray,
+ content: @Composable BoxScope.() -> Unit
+) = BoxWithConstraints(modifier) {
+ val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+ val leftActions = if (isRtl) endActions else startActions
+ val rightActions = if (isRtl) startActions else endActions
+ val swipeThresholdPx = LocalDensity.current.run { swipeThreshold.toPx() }
+
+ val ripple = remember {
+ SwipeRippleState()
+ }
+ val actions = remember(leftActions, rightActions) {
+ ActionFinder(left = leftActions, right = rightActions)
+ }
+ LaunchedEffect(state, actions) {
+ state.run {
+ canSwipeTowardsRight = { leftActions.isNotEmpty() }
+ canSwipeTowardsLeft = { rightActions.isNotEmpty() }
+ }
+ }
+
+ val offset = state.offset.value
+ val thresholdCrossed = abs(offset) > swipeThresholdPx
+
+ var swipedAction: SwipeActionMeta? by remember {
+ mutableStateOf(null)
+ }
+ val visibleAction: SwipeActionMeta? = remember(offset, actions) {
+ actions.actionAt(offset, totalWidth = constraints.maxWidth)
+ }
+ val backgroundColor: Color by animateColorAsState(
+ when {
+ swipedAction != null -> swipedAction!!.value.background
+ !thresholdCrossed -> backgroundUntilSwipeThreshold
+ visibleAction == null -> Color.Transparent
+ else -> visibleAction.value.background
+ }
+ )
+
+ val scope = rememberCoroutineScope()
+ Box(
+ modifier = Modifier
+ .absoluteOffset { IntOffset(x = offset.roundToInt(), y = 0) }
+ .drawOverContent { ripple.draw(scope = this) }
+ .draggable(
+ orientation = Horizontal,
+ enabled = !state.isResettingOnRelease,
+ onDragStopped = {
+ scope.launch {
+ if (thresholdCrossed && visibleAction != null) {
+ swipedAction = visibleAction
+ swipedAction!!.value.onSwipe()
+ ripple.animate(action = swipedAction!!)
+ }
+ }
+ scope.launch {
+ state.resetOffset()
+ swipedAction = null
+ }
+ },
+ state = state.draggableState,
+ ),
+ content = content
+ )
+
+ (swipedAction ?: visibleAction)?.let { action ->
+ ActionIconBox(
+ modifier = Modifier.matchParentSize(),
+ action = action,
+ offset = offset,
+ backgroundColor = backgroundColor,
+ content = { action.value.icon() }
+ )
+ }
+}
+
+@Composable
+private fun ActionIconBox(
+ action: SwipeActionMeta,
+ offset: Float,
+ backgroundColor: Color,
+ modifier: Modifier = Modifier,
+ content: @Composable () -> Unit
+) {
+ Row(
+ modifier = modifier
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(width = placeable.width, height = placeable.height) {
+ // Align icon with the left/right edge of the content being swiped.
+ val iconOffset = if (action.isOnRightSide) constraints.maxWidth + offset else offset - placeable.width
+ placeable.placeRelative(x = iconOffset.roundToInt(), y = 0)
+ }
+ }
+ .background(color = backgroundColor),
+ horizontalArrangement = if (action.isOnRightSide) Arrangement.Start else Arrangement.End,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ content()
+ }
+}
+
+private fun Modifier.drawOverContent(onDraw: DrawScope.() -> Unit): Modifier {
+ return drawWithContent {
+ drawContent()
+ onDraw(this)
+ }
+}
diff --git a/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeableActionsState.kt b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeableActionsState.kt
new file mode 100644
index 0000000000..d449928757
--- /dev/null
+++ b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/SwipeableActionsState.kt
@@ -0,0 +1,62 @@
+package me.saket.swipe
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.gestures.DraggableState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+
+@Composable
+fun rememberSwipeableActionsState(): SwipeableActionsState {
+ return remember { SwipeableActionsState() }
+}
+
+/**
+ * The state of a [SwipeableActionsBox].
+ */
+@Stable
+class SwipeableActionsState internal constructor() {
+ /**
+ * The current position (in pixels) of a [SwipeableActionsBox].
+ */
+ val offset: State get() = offsetState
+ internal var offsetState = mutableStateOf(0f)
+
+ /**
+ * Whether [SwipeableActionsBox] is currently animating to reset its offset after it was swiped.
+ */
+ var isResettingOnRelease: Boolean by mutableStateOf(false)
+ private set
+
+ internal lateinit var canSwipeTowardsRight: () -> Boolean
+ internal lateinit var canSwipeTowardsLeft: () -> Boolean
+
+ internal val draggableState = DraggableState { delta ->
+ val targetOffset = offsetState.value + delta
+ val isAllowed = isResettingOnRelease
+ || targetOffset > 0f && canSwipeTowardsRight()
+ || targetOffset < 0f && canSwipeTowardsLeft()
+
+ // Add some resistance if needed.
+ offsetState.value += if (isAllowed) delta else delta / 10
+ }
+
+ internal suspend fun resetOffset() {
+ draggableState.drag(MutatePriority.PreventUserInput) {
+ isResettingOnRelease = true
+ try {
+ Animatable(offsetState.value).animateTo(targetValue = 0f, tween(durationMillis = animationDurationMs)) {
+ dragBy(value - offsetState.value)
+ }
+ } finally {
+ isResettingOnRelease = false
+ }
+ }
+ }
+}
diff --git a/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/defaults.kt b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/defaults.kt
new file mode 100644
index 0000000000..04065de5d4
--- /dev/null
+++ b/thirdparty/swipe/src/commonMain/kotlin/me/saket/swipe/defaults.kt
@@ -0,0 +1,3 @@
+package me.saket.swipe
+
+internal const val animationDurationMs = 4_00
diff --git a/ui/account/build.gradle.kts b/ui/account/build.gradle.kts
index 47588c3340..daf3293fad 100644
--- a/ui/account/build.gradle.kts
+++ b/ui/account/build.gradle.kts
@@ -4,33 +4,32 @@
plugins {
id("app.tivi.android.library")
- id("app.tivi.android.compose")
- id("app.tivi.kotlin.android")
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
}
android {
namespace = "app.tivi.account"
}
-dependencies {
- implementation(projects.core.base)
- implementation(projects.domain)
- implementation(projects.common.ui.compose)
- implementation(projects.data.traktauth) // This should really be used through an interactor
-
- api(projects.common.ui.screens)
- api(projects.common.ui.circuitOverlay) // Only for LocalNavigator
- api(libs.circuit.foundation)
-
- // For registerForActivityResult
- implementation(libs.androidx.activity.compose)
-
- implementation(libs.compose.foundation.foundation)
- implementation(libs.compose.foundation.layout)
- implementation(libs.compose.material.material)
- implementation(libs.compose.material3.material3)
- implementation(libs.compose.animation.animation)
- implementation(libs.compose.ui.tooling)
-
- implementation(libs.coil.compose)
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(projects.core.base)
+ implementation(projects.domain)
+ implementation(projects.common.ui.compose)
+ implementation(projects.data.traktauth) // This should really be used through an interactor
+
+ api(projects.common.ui.screens)
+ api(projects.common.ui.circuitOverlay) // Only for LocalNavigator
+ api(libs.circuit.foundation)
+
+ implementation(compose.foundation)
+ implementation(compose.material)
+ implementation(compose.material3)
+ implementation(compose.animation)
+ }
+ }
+ }
}
diff --git a/ui/account/src/main/java/app/tivi/account/AccountComponent.kt b/ui/account/src/commonMain/kotlin/app/tivi/account/AccountComponent.kt
similarity index 87%
rename from ui/account/src/main/java/app/tivi/account/AccountComponent.kt
rename to ui/account/src/commonMain/kotlin/app/tivi/account/AccountComponent.kt
index 1d40e32a1c..97acd6433c 100644
--- a/ui/account/src/main/java/app/tivi/account/AccountComponent.kt
+++ b/ui/account/src/commonMain/kotlin/app/tivi/account/AccountComponent.kt
@@ -3,7 +3,7 @@
package app.tivi.account
-import app.tivi.inject.ApplicationScope
+import app.tivi.inject.ActivityScope
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.ui.Ui
import me.tatarka.inject.annotations.IntoSet
@@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides
interface AccountComponent {
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindAccountPresenterFactory(factory: AccountUiPresenterFactory): Presenter.Factory = factory
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindAccountUiFactory(factory: AccountUiFactory): Ui.Factory = factory
}
diff --git a/ui/account/src/main/java/app/tivi/account/AccountPresenter.kt b/ui/account/src/commonMain/kotlin/app/tivi/account/AccountPresenter.kt
similarity index 100%
rename from ui/account/src/main/java/app/tivi/account/AccountPresenter.kt
rename to ui/account/src/commonMain/kotlin/app/tivi/account/AccountPresenter.kt
diff --git a/ui/account/src/main/java/app/tivi/account/AccountUi.kt b/ui/account/src/commonMain/kotlin/app/tivi/account/AccountUi.kt
similarity index 98%
rename from ui/account/src/main/java/app/tivi/account/AccountUi.kt
rename to ui/account/src/commonMain/kotlin/app/tivi/account/AccountUi.kt
index 9805473e52..2f3f99bf32 100644
--- a/ui/account/src/main/java/app/tivi/account/AccountUi.kt
+++ b/ui/account/src/commonMain/kotlin/app/tivi/account/AccountUi.kt
@@ -32,7 +32,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import app.tivi.common.compose.ui.AsyncImage
import app.tivi.common.ui.resources.MR
@@ -174,7 +173,7 @@ private fun UserRow(
if (avatarUrl != null) {
AsyncImage(
model = avatarUrl,
- requestBuilder = { crossfade(true) },
+
contentDescription = stringResource(
MR.strings.cd_profile_pic,
user.name
@@ -234,7 +233,7 @@ private fun AppAction(
}
}
-@Preview
+// @Preview
@Composable
fun PreviewUserRow() {
UserRow(
diff --git a/ui/account/src/main/java/app/tivi/account/AccountUiState.kt b/ui/account/src/commonMain/kotlin/app/tivi/account/AccountUiState.kt
similarity index 100%
rename from ui/account/src/main/java/app/tivi/account/AccountUiState.kt
rename to ui/account/src/commonMain/kotlin/app/tivi/account/AccountUiState.kt
diff --git a/ui/account/src/main/AndroidManifest.xml b/ui/account/src/main/AndroidManifest.xml
deleted file mode 100644
index 3e33db4e7f..0000000000
--- a/ui/account/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
diff --git a/ui/discover/build.gradle.kts b/ui/discover/build.gradle.kts
index 2ff5bf1d62..3598edbe5a 100644
--- a/ui/discover/build.gradle.kts
+++ b/ui/discover/build.gradle.kts
@@ -4,30 +4,32 @@
plugins {
id("app.tivi.android.library")
- id("app.tivi.android.compose")
- id("app.tivi.kotlin.android")
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
}
android {
namespace = "app.tivi.home.discover"
}
-dependencies {
- implementation(projects.core.base)
- implementation(projects.domain)
- implementation(projects.common.ui.compose)
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(projects.core.base)
+ implementation(projects.domain)
+ implementation(projects.common.ui.compose)
- api(projects.common.ui.screens)
- api(projects.common.ui.circuitOverlay)
- api(libs.circuit.foundation)
+ api(projects.common.ui.screens)
+ api(projects.common.ui.circuitOverlay)
+ api(libs.circuit.foundation)
- implementation(libs.androidx.activity.compose)
-
- implementation(libs.compose.foundation.foundation)
- implementation(libs.compose.foundation.layout)
- implementation(libs.compose.material.material)
- implementation(libs.compose.material3.material3)
- implementation(libs.compose.material3.windowsizeclass)
- implementation(libs.compose.animation.animation)
- implementation(libs.compose.ui.tooling)
+ implementation(compose.foundation)
+ implementation(compose.material)
+ implementation(compose.material3)
+ implementation(libs.compose.material3.windowsizeclass)
+ implementation(compose.animation)
+ }
+ }
+ }
}
diff --git a/ui/discover/src/main/java/app/tivi/home/discover/Discover.kt b/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/Discover.kt
similarity index 94%
rename from ui/discover/src/main/java/app/tivi/home/discover/Discover.kt
rename to ui/discover/src/commonMain/kotlin/app/tivi/home/discover/Discover.kt
index 938e230e3d..a070127abd 100644
--- a/ui/discover/src/main/java/app/tivi/home/discover/Discover.kt
+++ b/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/Discover.kt
@@ -5,8 +5,6 @@
package app.tivi.home.discover
-import android.os.Build
-import androidx.activity.compose.ReportDrawnWhen
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
@@ -28,13 +26,15 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.DismissValue
import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
+import androidx.compose.material.rememberDismissState
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
-import androidx.compose.material3.DismissValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@@ -42,10 +42,8 @@ import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
-import androidx.compose.material3.SwipeToDismiss
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
-import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
@@ -53,10 +51,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.FirstBaseline
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import app.tivi.common.compose.Layout
import app.tivi.common.compose.LocalTiviTextCreator
+import app.tivi.common.compose.ReportDrawnWhen
import app.tivi.common.compose.bodyWidth
import app.tivi.common.compose.rememberCoroutineScope
import app.tivi.common.compose.ui.AutoSizedCircularProgressIndicator
@@ -139,16 +137,14 @@ internal fun Discover(
) {
val snackbarHostState = remember { SnackbarHostState() }
- val dismissSnackbarState = rememberDismissState(
- confirmValueChange = { value ->
- if (value != DismissValue.Default) {
- snackbarHostState.currentSnackbarData?.dismiss()
- true
- } else {
- false
- }
- },
- )
+ val dismissSnackbarState = rememberDismissState { value ->
+ if (value != DismissValue.Default) {
+ snackbarHostState.currentSnackbarData?.dismiss()
+ true
+ } else {
+ false
+ }
+ }
state.message?.let { message ->
LaunchedEffect(message) {
@@ -158,15 +154,11 @@ internal fun Discover(
}
}
- if (Build.VERSION.SDK_INT >= 25) {
- // ReportDrawnWhen routinely causes crashes on API < 25:
- // https://issuetracker.google.com/issues/260506820
- ReportDrawnWhen {
- !state.popularRefreshing &&
- !state.trendingRefreshing &&
- state.popularItems.isNotEmpty() &&
- state.trendingItems.isNotEmpty()
- }
+ ReportDrawnWhen {
+ !state.popularRefreshing &&
+ !state.trendingRefreshing &&
+ state.popularItems.isNotEmpty() &&
+ state.trendingItems.isNotEmpty()
}
Scaffold(
@@ -445,7 +437,7 @@ private fun Header(
}
}
-@Preview
+// @Preview
@Composable
private fun PreviewHeader() {
Surface(Modifier.fillMaxWidth()) {
diff --git a/ui/discover/src/main/java/app/tivi/home/discover/DiscoverComponent.kt b/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/DiscoverComponent.kt
similarity index 87%
rename from ui/discover/src/main/java/app/tivi/home/discover/DiscoverComponent.kt
rename to ui/discover/src/commonMain/kotlin/app/tivi/home/discover/DiscoverComponent.kt
index 0cf1001a87..6b7652130b 100644
--- a/ui/discover/src/main/java/app/tivi/home/discover/DiscoverComponent.kt
+++ b/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/DiscoverComponent.kt
@@ -3,7 +3,7 @@
package app.tivi.home.discover
-import app.tivi.inject.ApplicationScope
+import app.tivi.inject.ActivityScope
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.ui.Ui
import me.tatarka.inject.annotations.IntoSet
@@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides
interface DiscoverComponent {
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindDiscoverPresenterFactory(factory: DiscoverUiPresenterFactory): Presenter.Factory = factory
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindDiscoverUiFactoryFactory(factory: DiscoverUiFactory): Ui.Factory = factory
}
diff --git a/ui/discover/src/main/java/app/tivi/home/discover/DiscoverPresenter.kt b/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/DiscoverPresenter.kt
similarity index 100%
rename from ui/discover/src/main/java/app/tivi/home/discover/DiscoverPresenter.kt
rename to ui/discover/src/commonMain/kotlin/app/tivi/home/discover/DiscoverPresenter.kt
diff --git a/ui/discover/src/main/java/app/tivi/home/discover/DiscoverUiState.kt b/ui/discover/src/commonMain/kotlin/app/tivi/home/discover/DiscoverUiState.kt
similarity index 100%
rename from ui/discover/src/main/java/app/tivi/home/discover/DiscoverUiState.kt
rename to ui/discover/src/commonMain/kotlin/app/tivi/home/discover/DiscoverUiState.kt
diff --git a/ui/episode/details/build.gradle.kts b/ui/episode/details/build.gradle.kts
index 6d5668d030..b7d03e69ee 100644
--- a/ui/episode/details/build.gradle.kts
+++ b/ui/episode/details/build.gradle.kts
@@ -4,31 +4,33 @@
plugins {
id("app.tivi.android.library")
- id("app.tivi.android.compose")
- id("app.tivi.kotlin.android")
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
}
android {
namespace = "app.tivi.episodedetails"
}
-dependencies {
- implementation(projects.core.base)
- implementation(projects.domain)
- implementation(projects.common.ui.compose)
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(projects.core.base)
+ implementation(projects.domain)
+ implementation(projects.common.ui.compose)
- api(projects.common.ui.screens)
- api(projects.common.ui.circuitOverlay)
- api(libs.circuit.foundation)
+ api(projects.common.ui.screens)
+ api(projects.common.ui.circuitOverlay)
+ api(libs.circuit.foundation)
- implementation(libs.compose.foundation.foundation)
- implementation(libs.compose.foundation.layout)
- implementation(libs.compose.material.material)
- implementation(libs.compose.material3.material3)
- implementation(libs.compose.material3.windowsizeclass)
- implementation(libs.compose.material.iconsext)
- implementation(libs.compose.animation.animation)
- implementation(libs.compose.ui.tooling)
-
- implementation(libs.coil.compose)
+ implementation(compose.foundation)
+ implementation(compose.material)
+ implementation(compose.material3)
+ implementation(libs.compose.material3.windowsizeclass)
+ implementation(compose.materialIconsExtended)
+ implementation(compose.animation)
+ }
+ }
+ }
}
diff --git a/ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetails.kt b/ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetails.kt
similarity index 93%
rename from ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetails.kt
rename to ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetails.kt
index bf0bcd52a8..73465a166b 100644
--- a/ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetails.kt
+++ b/ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetails.kt
@@ -18,10 +18,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.DismissDirection
+import androidx.compose.material.DismissValue
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.CalendarToday
@@ -31,9 +34,8 @@ import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material.rememberDismissState
import androidx.compose.material3.Button
-import androidx.compose.material3.DismissDirection
-import androidx.compose.material3.DismissValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -44,11 +46,9 @@ import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
-import androidx.compose.material3.SwipeToDismiss
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -62,7 +62,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import app.tivi.common.compose.Layout
import app.tivi.common.compose.LocalTiviDateFormatter
@@ -80,9 +79,10 @@ import app.tivi.data.models.Episode
import app.tivi.data.models.EpisodeWatchEntry
import app.tivi.data.models.PendingAction
import app.tivi.data.models.Season
-import app.tivi.overlays.showInBottomSheet
+import app.tivi.overlays.showInDialog
import app.tivi.screens.EpisodeDetailsScreen
import app.tivi.screens.EpisodeTrackScreen
+import com.moriatsushi.insetsx.statusBars
import com.slack.circuit.overlay.LocalOverlayHost
import com.slack.circuit.runtime.CircuitContext
import com.slack.circuit.runtime.Screen
@@ -122,7 +122,7 @@ internal fun EpisodeDetails(
onRemoveWatch = { id -> viewState.eventSink(EpisodeDetailsUiEvent.RemoveWatchEntry(id)) },
onAddWatch = {
scope.launch {
- overlayHost.showInBottomSheet(EpisodeTrackScreen(viewState.episode!!.id))
+ overlayHost.showInDialog(EpisodeTrackScreen(viewState.episode!!.id))
}
},
onMessageShown = { id -> viewState.eventSink(EpisodeDetailsUiEvent.ClearMessage(id)) },
@@ -130,7 +130,7 @@ internal fun EpisodeDetails(
)
}
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
internal fun EpisodeDetails(
viewState: EpisodeDetailsUiState,
@@ -144,16 +144,14 @@ internal fun EpisodeDetails(
) {
val snackbarHostState = remember { SnackbarHostState() }
- val dismissSnackbarState = rememberDismissState(
- confirmValueChange = { value ->
- if (value != DismissValue.Default) {
- snackbarHostState.currentSnackbarData?.dismiss()
- true
- } else {
- false
- }
- },
- )
+ val dismissSnackbarState = rememberDismissState { value ->
+ if (value != DismissValue.Default) {
+ snackbarHostState.currentSnackbarData?.dismiss()
+ true
+ } else {
+ false
+ }
+ }
viewState.message?.let { message ->
LaunchedEffect(message) {
@@ -249,16 +247,14 @@ internal fun EpisodeDetails(
viewState.watches.forEach { watch ->
key(watch.id) {
- val dismissState = rememberDismissState(
- confirmValueChange = { value ->
- if (value != DismissValue.Default) {
- onRemoveWatch(watch.id)
- true
- } else {
- false
- }
- },
- )
+ val dismissState = rememberDismissState { value ->
+ if (value != DismissValue.Default) {
+ onRemoveWatch(watch.id)
+ true
+ } else {
+ false
+ }
+ }
SwipeToDismiss(
state = dismissState,
@@ -523,7 +519,7 @@ private fun EpisodeDetailsAppBar(
modifier: Modifier = Modifier,
) {
TopAppBar(
- colors = TopAppBarDefaults.topAppBarColors(
+ colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = Color.Transparent,
actionIconContentColor = LocalContentColor.current,
),
@@ -558,7 +554,7 @@ private fun EpisodeDetailsAppBar(
)
}
-@Preview
+// @Preview
@Composable
fun PreviewEpisodeDetails() {
EpisodeDetails(
diff --git a/ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetailsComponent.kt b/ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetailsComponent.kt
similarity index 88%
rename from ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetailsComponent.kt
rename to ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetailsComponent.kt
index f63c42c86c..0262fa1a8e 100644
--- a/ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetailsComponent.kt
+++ b/ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetailsComponent.kt
@@ -3,7 +3,7 @@
package app.tivi.episodedetails
-import app.tivi.inject.ApplicationScope
+import app.tivi.inject.ActivityScope
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.ui.Ui
import me.tatarka.inject.annotations.IntoSet
@@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides
interface EpisodeDetailsComponent {
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindEpisodeDetailsPresenterFactory(factory: EpisodeDetailsUiPresenterFactory): Presenter.Factory = factory
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindEpisodeDetailsUiFactoryFactory(factory: EpisodeDetailsUiFactory): Ui.Factory = factory
}
diff --git a/ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetailsPresenter.kt b/ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetailsPresenter.kt
similarity index 100%
rename from ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetailsPresenter.kt
rename to ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetailsPresenter.kt
diff --git a/ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetailsUiState.kt b/ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetailsUiState.kt
similarity index 100%
rename from ui/episode/details/src/main/java/app/tivi/episodedetails/EpisodeDetailsUiState.kt
rename to ui/episode/details/src/commonMain/kotlin/app/tivi/episodedetails/EpisodeDetailsUiState.kt
diff --git a/ui/episode/track/build.gradle.kts b/ui/episode/track/build.gradle.kts
index 56c2ec4b65..fc2c67fc94 100644
--- a/ui/episode/track/build.gradle.kts
+++ b/ui/episode/track/build.gradle.kts
@@ -4,30 +4,32 @@
plugins {
id("app.tivi.android.library")
- id("app.tivi.android.compose")
- id("app.tivi.kotlin.android")
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
}
android {
namespace = "app.tivi.episode.track"
}
-dependencies {
- implementation(projects.core.base)
- implementation(projects.domain)
- implementation(projects.common.ui.compose)
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(projects.core.base)
+ implementation(projects.domain)
+ implementation(projects.common.ui.compose)
- api(projects.common.ui.screens)
- api(libs.circuit.foundation)
+ api(projects.common.ui.screens)
+ api(libs.circuit.foundation)
- implementation(libs.compose.foundation.foundation)
- implementation(libs.compose.foundation.layout)
- implementation(libs.compose.material.material)
- implementation(libs.compose.material3.material3)
- implementation(libs.compose.material3.windowsizeclass)
- implementation(libs.compose.material.iconsext)
- implementation(libs.compose.animation.animation)
- implementation(libs.compose.ui.tooling)
-
- implementation(libs.coil.compose)
+ implementation(compose.foundation)
+ implementation(compose.material)
+ implementation(compose.material3)
+ implementation(libs.compose.material3.windowsizeclass)
+ implementation(compose.materialIconsExtended)
+ implementation(compose.animation)
+ }
+ }
+ }
}
diff --git a/ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrack.kt b/ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrack.kt
similarity index 94%
rename from ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrack.kt
rename to ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrack.kt
index 5cc0ffa66a..f7f3935ed1 100644
--- a/ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrack.kt
+++ b/ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrack.kt
@@ -13,19 +13,19 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
+import androidx.compose.material.DismissValue
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.SwipeToDismiss
+import androidx.compose.material.rememberDismissState
import androidx.compose.material3.Card
-import androidx.compose.material3.DismissValue
import androidx.compose.material3.Divider
-import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.SwipeToDismiss
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
-import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
@@ -94,7 +94,7 @@ internal fun EpisodeTrack(
)
}
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun EpisodeTrack(
viewState: EpisodeTrackUiState,
@@ -109,16 +109,14 @@ internal fun EpisodeTrack(
) {
val snackbarHostState = remember { SnackbarHostState() }
- val dismissSnackbarState = rememberDismissState(
- confirmValueChange = { value ->
- if (value != DismissValue.Default) {
- snackbarHostState.currentSnackbarData?.dismiss()
- true
- } else {
- false
- }
- },
- )
+ val dismissSnackbarState = rememberDismissState { value ->
+ if (value != DismissValue.Default) {
+ snackbarHostState.currentSnackbarData?.dismiss()
+ true
+ } else {
+ false
+ }
+ }
viewState.message?.let { message ->
LaunchedEffect(message) {
@@ -198,7 +196,6 @@ private fun EpisodeHeader(
) {
AsyncImage(
model = episode.asImageModel(),
- requestBuilder = { crossfade(true) },
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop,
diff --git a/ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrackComponent.kt b/ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrackComponent.kt
similarity index 88%
rename from ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrackComponent.kt
rename to ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrackComponent.kt
index 1575021bb2..70fc3f817b 100644
--- a/ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrackComponent.kt
+++ b/ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrackComponent.kt
@@ -3,7 +3,7 @@
package app.tivi.episode.track
-import app.tivi.inject.ApplicationScope
+import app.tivi.inject.ActivityScope
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.ui.Ui
import me.tatarka.inject.annotations.IntoSet
@@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides
interface EpisodeTrackComponent {
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindEpisodeTrackPresenterFactory(factory: EpisodeTrackUiPresenterFactory): Presenter.Factory = factory
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindEpisodeTrackUiFactoryFactory(factory: EpisodeTrackUiFactory): Ui.Factory = factory
}
diff --git a/ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrackPresenter.kt b/ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrackPresenter.kt
similarity index 100%
rename from ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrackPresenter.kt
rename to ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrackPresenter.kt
diff --git a/ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrackUiState.kt b/ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrackUiState.kt
similarity index 100%
rename from ui/episode/track/src/main/java/app/tivi/episode/track/EpisodeTrackUiState.kt
rename to ui/episode/track/src/commonMain/kotlin/app/tivi/episode/track/EpisodeTrackUiState.kt
diff --git a/ui/library/build.gradle.kts b/ui/library/build.gradle.kts
index f41d0c13fb..e80ab9a6eb 100644
--- a/ui/library/build.gradle.kts
+++ b/ui/library/build.gradle.kts
@@ -4,34 +4,34 @@
plugins {
id("app.tivi.android.library")
- id("app.tivi.android.compose")
- id("app.tivi.kotlin.android")
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
}
android {
namespace = "app.tivi.home.shows"
}
-dependencies {
- implementation(projects.core.base)
- implementation(projects.domain)
- implementation(projects.common.ui.compose)
-
- api(projects.common.ui.screens)
- api(projects.common.ui.circuitOverlay)
- api(libs.circuit.foundation)
-
- implementation(libs.paging.compose)
-
- implementation(libs.androidx.core)
-
- implementation(libs.compose.foundation.foundation)
- implementation(libs.compose.foundation.layout)
- implementation(libs.compose.material.material)
- implementation(libs.compose.material.iconsext)
- implementation(libs.compose.material3.material3)
- implementation(libs.compose.animation.animation)
- implementation(libs.compose.ui.tooling)
-
- implementation(libs.coil.compose)
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(projects.core.base)
+ implementation(projects.domain)
+ implementation(projects.common.ui.compose)
+
+ api(projects.common.ui.screens)
+ api(projects.common.ui.circuitOverlay)
+ api(libs.circuit.foundation)
+
+ implementation(libs.paging.compose)
+
+ implementation(compose.foundation)
+ implementation(compose.material)
+ implementation(compose.materialIconsExtended)
+ implementation(compose.material3)
+ implementation(compose.animation)
+ }
+ }
+ }
}
diff --git a/ui/library/src/main/java/app/tivi/home/library/Library.kt b/ui/library/src/commonMain/kotlin/app/tivi/home/library/Library.kt
similarity index 97%
rename from ui/library/src/main/java/app/tivi/home/library/Library.kt
rename to ui/library/src/commonMain/kotlin/app/tivi/home/library/Library.kt
index b2bfb745ee..8f8d05a5fc 100644
--- a/ui/library/src/main/java/app/tivi/home/library/Library.kt
+++ b/ui/library/src/commonMain/kotlin/app/tivi/home/library/Library.kt
@@ -24,14 +24,16 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material.DismissValue
import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
-import androidx.compose.material3.DismissValue
+import androidx.compose.material.rememberDismissState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
@@ -42,10 +44,8 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.SwipeToDismiss
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -152,16 +152,14 @@ internal fun Library(
) {
val snackbarHostState = remember { SnackbarHostState() }
- val dismissSnackbarState = rememberDismissState(
- confirmValueChange = { value ->
- if (value != DismissValue.Default) {
- snackbarHostState.currentSnackbarData?.dismiss()
- true
- } else {
- false
- }
- },
- )
+ val dismissSnackbarState = rememberDismissState { value ->
+ if (value != DismissValue.Default) {
+ snackbarHostState.currentSnackbarData?.dismiss()
+ true
+ } else {
+ false
+ }
+ }
state.message?.let { message ->
LaunchedEffect(message) {
diff --git a/ui/library/src/main/java/app/tivi/home/library/LibraryComponent.kt b/ui/library/src/commonMain/kotlin/app/tivi/home/library/LibraryComponent.kt
similarity index 87%
rename from ui/library/src/main/java/app/tivi/home/library/LibraryComponent.kt
rename to ui/library/src/commonMain/kotlin/app/tivi/home/library/LibraryComponent.kt
index 0b22632edd..7054fcb1c6 100644
--- a/ui/library/src/main/java/app/tivi/home/library/LibraryComponent.kt
+++ b/ui/library/src/commonMain/kotlin/app/tivi/home/library/LibraryComponent.kt
@@ -3,7 +3,7 @@
package app.tivi.home.library
-import app.tivi.inject.ApplicationScope
+import app.tivi.inject.ActivityScope
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.ui.Ui
import me.tatarka.inject.annotations.IntoSet
@@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides
interface LibraryComponent {
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindLibraryPresenterFactory(factory: LibraryUiPresenterFactory): Presenter.Factory = factory
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindLibraryUiFactoryFactory(factory: LibraryUiFactory): Ui.Factory = factory
}
diff --git a/ui/library/src/main/java/app/tivi/home/library/LibraryPresenter.kt b/ui/library/src/commonMain/kotlin/app/tivi/home/library/LibraryPresenter.kt
similarity index 100%
rename from ui/library/src/main/java/app/tivi/home/library/LibraryPresenter.kt
rename to ui/library/src/commonMain/kotlin/app/tivi/home/library/LibraryPresenter.kt
diff --git a/ui/library/src/main/java/app/tivi/home/library/LibraryUiState.kt b/ui/library/src/commonMain/kotlin/app/tivi/home/library/LibraryUiState.kt
similarity index 100%
rename from ui/library/src/main/java/app/tivi/home/library/LibraryUiState.kt
rename to ui/library/src/commonMain/kotlin/app/tivi/home/library/LibraryUiState.kt
diff --git a/ui/popular/build.gradle.kts b/ui/popular/build.gradle.kts
index 9f1bd5455b..64603f0ff2 100644
--- a/ui/popular/build.gradle.kts
+++ b/ui/popular/build.gradle.kts
@@ -4,27 +4,31 @@
plugins {
id("app.tivi.android.library")
- id("app.tivi.android.compose")
- id("app.tivi.kotlin.android")
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
}
android {
namespace = "app.tivi.home.popular"
}
-dependencies {
- implementation(projects.core.base)
- implementation(projects.domain)
- implementation(projects.common.ui.compose)
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(projects.core.base)
+ implementation(projects.domain)
+ implementation(projects.common.ui.compose)
- api(projects.common.ui.screens)
- api(libs.circuit.foundation)
+ api(projects.common.ui.screens)
+ api(libs.circuit.foundation)
- implementation(libs.paging.compose)
+ implementation(libs.paging.compose)
- implementation(libs.compose.foundation.foundation)
- implementation(libs.compose.foundation.layout)
- implementation(libs.compose.material.material)
- implementation(libs.compose.animation.animation)
- implementation(libs.compose.ui.tooling)
+ implementation(compose.foundation)
+ implementation(compose.material)
+ implementation(compose.animation)
+ }
+ }
+ }
}
diff --git a/ui/popular/src/main/java/app/tivi/home/popular/PopularShows.kt b/ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShows.kt
similarity index 100%
rename from ui/popular/src/main/java/app/tivi/home/popular/PopularShows.kt
rename to ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShows.kt
diff --git a/ui/popular/src/main/java/app/tivi/home/popular/PopularShowsComponent.kt b/ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShowsComponent.kt
similarity index 88%
rename from ui/popular/src/main/java/app/tivi/home/popular/PopularShowsComponent.kt
rename to ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShowsComponent.kt
index d462600315..62707d203c 100644
--- a/ui/popular/src/main/java/app/tivi/home/popular/PopularShowsComponent.kt
+++ b/ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShowsComponent.kt
@@ -3,7 +3,7 @@
package app.tivi.home.popular
-import app.tivi.inject.ApplicationScope
+import app.tivi.inject.ActivityScope
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.ui.Ui
import me.tatarka.inject.annotations.IntoSet
@@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides
interface PopularShowsComponent {
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindPopularShowsPresenterFactory(factory: PopularShowsUiPresenterFactory): Presenter.Factory = factory
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindPopularShowsUiFactoryFactory(factory: PopularShowsUiFactory): Ui.Factory = factory
}
diff --git a/ui/popular/src/main/java/app/tivi/home/popular/PopularShowsPresenter.kt b/ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShowsPresenter.kt
similarity index 100%
rename from ui/popular/src/main/java/app/tivi/home/popular/PopularShowsPresenter.kt
rename to ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShowsPresenter.kt
diff --git a/ui/popular/src/main/java/app/tivi/home/popular/PopularShowsUiState.kt b/ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShowsUiState.kt
similarity index 100%
rename from ui/popular/src/main/java/app/tivi/home/popular/PopularShowsUiState.kt
rename to ui/popular/src/commonMain/kotlin/app/tivi/home/popular/PopularShowsUiState.kt
diff --git a/ui/recommended/build.gradle.kts b/ui/recommended/build.gradle.kts
index 30a7cef663..99a46f20f8 100644
--- a/ui/recommended/build.gradle.kts
+++ b/ui/recommended/build.gradle.kts
@@ -4,27 +4,31 @@
plugins {
id("app.tivi.android.library")
- id("app.tivi.android.compose")
- id("app.tivi.kotlin.android")
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
}
android {
namespace = "app.tivi.home.recommended"
}
-dependencies {
- implementation(projects.core.base)
- implementation(projects.domain)
- implementation(projects.common.ui.compose)
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(projects.core.base)
+ implementation(projects.domain)
+ implementation(projects.common.ui.compose)
- api(projects.common.ui.screens)
- api(libs.circuit.foundation)
+ api(projects.common.ui.screens)
+ api(libs.circuit.foundation)
- implementation(libs.paging.compose)
+ implementation(libs.paging.compose)
- implementation(libs.compose.foundation.foundation)
- implementation(libs.compose.foundation.layout)
- implementation(libs.compose.material.material)
- implementation(libs.compose.animation.animation)
- implementation(libs.compose.ui.tooling)
+ implementation(compose.foundation)
+ implementation(compose.material)
+ implementation(compose.animation)
+ }
+ }
+ }
}
diff --git a/ui/recommended/src/main/java/app/tivi/home/recommended/Recommended.kt b/ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/Recommended.kt
similarity index 100%
rename from ui/recommended/src/main/java/app/tivi/home/recommended/Recommended.kt
rename to ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/Recommended.kt
diff --git a/ui/recommended/src/main/java/app/tivi/home/recommended/RecommendedShowsComponent.kt b/ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/RecommendedShowsComponent.kt
similarity index 88%
rename from ui/recommended/src/main/java/app/tivi/home/recommended/RecommendedShowsComponent.kt
rename to ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/RecommendedShowsComponent.kt
index 484ab664c1..03cd8f7b5c 100644
--- a/ui/recommended/src/main/java/app/tivi/home/recommended/RecommendedShowsComponent.kt
+++ b/ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/RecommendedShowsComponent.kt
@@ -3,7 +3,7 @@
package app.tivi.home.recommended
-import app.tivi.inject.ApplicationScope
+import app.tivi.inject.ActivityScope
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.ui.Ui
import me.tatarka.inject.annotations.IntoSet
@@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides
interface RecommendedShowsComponent {
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindRecommendedShowsPresenterFactory(factory: RecommendedShowsUiPresenterFactory): Presenter.Factory = factory
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindRecommendedShowsUiFactoryFactory(factory: RecommendedShowsUiFactory): Ui.Factory = factory
}
diff --git a/ui/recommended/src/main/java/app/tivi/home/recommended/RecommendedShowsPresenter.kt b/ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/RecommendedShowsPresenter.kt
similarity index 100%
rename from ui/recommended/src/main/java/app/tivi/home/recommended/RecommendedShowsPresenter.kt
rename to ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/RecommendedShowsPresenter.kt
diff --git a/ui/recommended/src/main/java/app/tivi/home/recommended/RecommendedShowsUiState.kt b/ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/RecommendedShowsUiState.kt
similarity index 100%
rename from ui/recommended/src/main/java/app/tivi/home/recommended/RecommendedShowsUiState.kt
rename to ui/recommended/src/commonMain/kotlin/app/tivi/home/recommended/RecommendedShowsUiState.kt
diff --git a/ui/root/build.gradle.kts b/ui/root/build.gradle.kts
new file mode 100644
index 0000000000..07d2178b72
--- /dev/null
+++ b/ui/root/build.gradle.kts
@@ -0,0 +1,35 @@
+// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+
+plugins {
+ id("app.tivi.android.library")
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
+}
+
+android {
+ namespace = "app.tivi.home"
+}
+
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(projects.core.base)
+ implementation(projects.core.analytics)
+ implementation(projects.common.ui.compose)
+
+ implementation(projects.common.ui.screens)
+ implementation(libs.circuit.foundation)
+ implementation(libs.circuit.overlay)
+ implementation(projects.common.ui.circuitOverlay)
+
+ implementation(compose.foundation)
+ implementation(compose.material)
+ implementation(compose.materialIconsExtended)
+ implementation(compose.animation)
+ }
+ }
+ }
+}
diff --git a/android-app/app/src/main/java/app/tivi/home/Home.kt b/ui/root/src/commonMain/kotlin/app/tivi/home/Home.kt
similarity index 94%
rename from android-app/app/src/main/java/app/tivi/home/Home.kt
rename to ui/root/src/commonMain/kotlin/app/tivi/home/Home.kt
index 35e286ecf1..87d798325a 100644
--- a/android-app/app/src/main/java/app/tivi/home/Home.kt
+++ b/ui/root/src/commonMain/kotlin/app/tivi/home/Home.kt
@@ -12,10 +12,7 @@ import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeContentPadding
-import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsBottomHeight
@@ -27,6 +24,7 @@ import androidx.compose.material.icons.filled.Weekend
import androidx.compose.material.icons.outlined.VideoLibrary
import androidx.compose.material.icons.outlined.Weekend
import androidx.compose.material3.Divider
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
@@ -44,11 +42,8 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.unit.dp
import app.tivi.common.compose.LocalWindowSizeClass
import app.tivi.common.ui.resources.MR
@@ -56,6 +51,9 @@ import app.tivi.screens.DiscoverScreen
import app.tivi.screens.LibraryScreen
import app.tivi.screens.SearchScreen
import app.tivi.screens.UpNextScreen
+import com.moriatsushi.insetsx.navigationBars
+import com.moriatsushi.insetsx.safeContentPadding
+import com.moriatsushi.insetsx.statusBars
import com.slack.circuit.backstack.SaveableBackStack
import com.slack.circuit.foundation.NavigableCircuitContent
import com.slack.circuit.foundation.screen
@@ -65,11 +63,12 @@ import com.slack.circuit.runtime.Screen
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.stringResource
-@OptIn(ExperimentalComposeUiApi::class)
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun Home(
backstack: SaveableBackStack,
navigator: Navigator,
+ modifier: Modifier = Modifier,
) {
val windowSizeClass = LocalWindowSizeClass.current
val navigationType = remember(windowSizeClass) {
@@ -98,11 +97,7 @@ internal fun Home(
},
contentWindowInsets = ScaffoldDefaults.contentWindowInsets
.exclude(WindowInsets.statusBars), // We let content handle the status bar
- modifier = Modifier.semantics {
- // Enables testTag -> UiAutomator resource id
- // See https://developer.android.com/jetpack/compose/testing#uiautomator-interop
- testTagsAsResourceId = true
- },
+ modifier = modifier,
) { paddingValues ->
Row(
modifier = Modifier
@@ -202,6 +197,7 @@ internal fun HomeNavigationDrawer(
.widthIn(max = 280.dp),
) {
for (item in HomeNavigationItems) {
+ @OptIn(ExperimentalMaterial3Api::class)
NavigationDrawerItem(
icon = {
Icon(
diff --git a/ui/root/src/commonMain/kotlin/app/tivi/home/TiviContent.kt b/ui/root/src/commonMain/kotlin/app/tivi/home/TiviContent.kt
new file mode 100644
index 0000000000..7cef4bf3d4
--- /dev/null
+++ b/ui/root/src/commonMain/kotlin/app/tivi/home/TiviContent.kt
@@ -0,0 +1,118 @@
+// Copyright 2023, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.home
+
+import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
+import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import app.tivi.common.compose.LocalTiviDateFormatter
+import app.tivi.common.compose.LocalTiviTextCreator
+import app.tivi.common.compose.LocalWindowSizeClass
+import app.tivi.common.compose.shouldUseDarkColors
+import app.tivi.common.compose.shouldUseDynamicColors
+import app.tivi.common.compose.theme.TiviTheme
+import app.tivi.core.analytics.Analytics
+import app.tivi.overlays.LocalNavigator
+import app.tivi.screens.DiscoverScreen
+import app.tivi.screens.SettingsScreen
+import app.tivi.screens.TiviScreen
+import app.tivi.settings.TiviPreferences
+import app.tivi.util.TiviDateFormatter
+import app.tivi.util.TiviTextCreator
+import com.seiko.imageloader.ImageLoader
+import com.seiko.imageloader.LocalImageLoader
+import com.slack.circuit.backstack.SaveableBackStack
+import com.slack.circuit.backstack.rememberSaveableBackStack
+import com.slack.circuit.foundation.CircuitCompositionLocals
+import com.slack.circuit.foundation.CircuitConfig
+import com.slack.circuit.foundation.push
+import com.slack.circuit.foundation.rememberCircuitNavigator
+import com.slack.circuit.foundation.screen
+import com.slack.circuit.runtime.Navigator
+import com.slack.circuit.runtime.Screen
+import me.tatarka.inject.annotations.Assisted
+import me.tatarka.inject.annotations.Inject
+
+typealias TiviContent = @Composable (
+ onRootPop: () -> Unit,
+ onOpenSettings: () -> Unit,
+ modifier: Modifier,
+) -> Unit
+
+@Inject
+@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
+@Composable
+fun TiviContent(
+ @Assisted onRootPop: () -> Unit,
+ @Assisted onOpenSettings: () -> Unit,
+ circuitConfig: CircuitConfig,
+ analytics: Analytics,
+ tiviDateFormatter: TiviDateFormatter,
+ tiviTextCreator: TiviTextCreator,
+ preferences: TiviPreferences,
+ imageLoader: ImageLoader,
+ @Assisted modifier: Modifier = Modifier,
+) {
+ val backstack: SaveableBackStack = rememberSaveableBackStack { push(DiscoverScreen) }
+ val circuitNavigator = rememberCircuitNavigator(backstack, onRootPop)
+
+ val navigator: Navigator = remember(circuitNavigator) {
+ TiviNavigator(circuitNavigator, onOpenSettings)
+ }
+
+ // Launch an effect to track changes to the current back stack entry, and push them
+ // as a screen views to analytics
+ LaunchedEffect(backstack.topRecord) {
+ val topScreen = backstack.topRecord?.screen as? TiviScreen
+ analytics.trackScreenView(
+ name = topScreen?.name ?: "unknown screen",
+ arguments = topScreen?.arguments,
+ )
+ }
+
+ CompositionLocalProvider(
+ LocalNavigator provides navigator,
+ LocalImageLoader provides imageLoader,
+ LocalTiviDateFormatter provides tiviDateFormatter,
+ LocalTiviTextCreator provides tiviTextCreator,
+ LocalWindowSizeClass provides calculateWindowSizeClass(),
+ ) {
+ CircuitCompositionLocals(circuitConfig) {
+ TiviTheme(
+ useDarkColors = preferences.shouldUseDarkColors(),
+ useDynamicColors = preferences.shouldUseDynamicColors(),
+ ) {
+ Home(
+ backstack = backstack,
+ navigator = navigator,
+ modifier = modifier,
+ )
+ }
+ }
+ }
+}
+
+private class TiviNavigator(
+ private val navigator: Navigator,
+ private val onOpenSettings: () -> Unit,
+) : Navigator {
+ override fun goTo(screen: Screen) {
+ when (screen) {
+ is SettingsScreen -> onOpenSettings()
+ else -> navigator.goTo(screen)
+ }
+ }
+
+ override fun pop(): Screen? {
+ return navigator.pop()
+ }
+
+ override fun resetRoot(newRoot: Screen): List {
+ return navigator.resetRoot(newRoot)
+ }
+}
diff --git a/ui/root/src/iosMain/kotlin/app/tivi/home/TiviUiViewController.kt b/ui/root/src/iosMain/kotlin/app/tivi/home/TiviUiViewController.kt
new file mode 100644
index 0000000000..ec28b0cc31
--- /dev/null
+++ b/ui/root/src/iosMain/kotlin/app/tivi/home/TiviUiViewController.kt
@@ -0,0 +1,28 @@
+// Copyright 2020, Google LLC, Christopher Banes and the Tivi project contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package app.tivi.home
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.window.ComposeUIViewController
+import me.tatarka.inject.annotations.Assisted
+import me.tatarka.inject.annotations.Inject
+import platform.UIKit.UIViewController
+
+typealias TiviUiViewController = (
+ onRootPop: () -> Unit,
+ onOpenSettings: () -> Unit,
+) -> UIViewController
+
+@Inject
+fun TiviUiViewController(
+ @Assisted onRootPop: () -> Unit,
+ @Assisted onOpenSettings: () -> Unit,
+ tiviContent: TiviContent,
+): UIViewController = ComposeUIViewController {
+ tiviContent(
+ onRootPop = onRootPop,
+ onOpenSettings = onOpenSettings,
+ modifier = Modifier,
+ )
+}
diff --git a/ui/search/build.gradle.kts b/ui/search/build.gradle.kts
index 311da49748..32c9f718d2 100644
--- a/ui/search/build.gradle.kts
+++ b/ui/search/build.gradle.kts
@@ -4,27 +4,31 @@
plugins {
id("app.tivi.android.library")
- id("app.tivi.android.compose")
- id("app.tivi.kotlin.android")
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
}
android {
namespace = "app.tivi.home.search"
}
-dependencies {
- implementation(projects.core.base)
- implementation(projects.domain)
- implementation(projects.common.ui.compose)
- implementation(projects.common.imageloading)
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(projects.core.base)
+ implementation(projects.domain)
+ implementation(projects.common.ui.compose)
+ implementation(projects.common.imageloading)
- api(projects.common.ui.screens)
- api(libs.circuit.foundation)
+ api(projects.common.ui.screens)
+ api(libs.circuit.foundation)
- implementation(libs.compose.foundation.foundation)
- implementation(libs.compose.foundation.layout)
- implementation(libs.compose.material.material)
- implementation(libs.compose.material3.material3)
- implementation(libs.compose.animation.animation)
- implementation(libs.compose.ui.tooling)
+ implementation(compose.foundation)
+ implementation(compose.material)
+ implementation(compose.material3)
+ implementation(compose.animation)
+ }
+ }
+ }
}
diff --git a/ui/search/src/main/java/app/tivi/home/search/Search.kt b/ui/search/src/commonMain/kotlin/app/tivi/home/search/Search.kt
similarity index 93%
rename from ui/search/src/main/java/app/tivi/home/search/Search.kt
rename to ui/search/src/commonMain/kotlin/app/tivi/home/search/Search.kt
index 5cafa11151..3ff192786b 100644
--- a/ui/search/src/main/java/app/tivi/home/search/Search.kt
+++ b/ui/search/src/commonMain/kotlin/app/tivi/home/search/Search.kt
@@ -15,12 +15,14 @@ import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
-import androidx.compose.material3.DismissValue
+import androidx.compose.material.DismissValue
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.SwipeToDismiss
+import androidx.compose.material.rememberDismissState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@@ -28,9 +30,7 @@ import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
-import androidx.compose.material3.SwipeToDismiss
import androidx.compose.material3.Text
-import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -51,6 +51,7 @@ import app.tivi.common.compose.ui.plus
import app.tivi.common.ui.resources.MR
import app.tivi.data.models.TiviShow
import app.tivi.screens.SearchScreen
+import com.moriatsushi.insetsx.statusBarsPadding
import com.slack.circuit.runtime.CircuitContext
import com.slack.circuit.runtime.Screen
import com.slack.circuit.runtime.ui.Ui
@@ -89,7 +90,7 @@ internal fun Search(
)
}
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
internal fun Search(
state: SearchUiState,
@@ -100,16 +101,14 @@ internal fun Search(
) {
val snackbarHostState = remember { SnackbarHostState() }
- val dismissSnackbarState = rememberDismissState(
- confirmValueChange = { value ->
- if (value != DismissValue.Default) {
- snackbarHostState.currentSnackbarData?.dismiss()
- true
- } else {
- false
- }
- },
- )
+ val dismissSnackbarState = rememberDismissState { value ->
+ if (value != DismissValue.Default) {
+ snackbarHostState.currentSnackbarData?.dismiss()
+ true
+ } else {
+ false
+ }
+ }
state.message?.let { message ->
LaunchedEffect(message) {
diff --git a/ui/search/src/main/java/app/tivi/home/search/SearchComponent.kt b/ui/search/src/commonMain/kotlin/app/tivi/home/search/SearchComponent.kt
similarity index 87%
rename from ui/search/src/main/java/app/tivi/home/search/SearchComponent.kt
rename to ui/search/src/commonMain/kotlin/app/tivi/home/search/SearchComponent.kt
index cf5f78e2c7..9cb086639a 100644
--- a/ui/search/src/main/java/app/tivi/home/search/SearchComponent.kt
+++ b/ui/search/src/commonMain/kotlin/app/tivi/home/search/SearchComponent.kt
@@ -3,7 +3,7 @@
package app.tivi.home.search
-import app.tivi.inject.ApplicationScope
+import app.tivi.inject.ActivityScope
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.ui.Ui
import me.tatarka.inject.annotations.IntoSet
@@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides
interface SearchComponent {
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindSearchPresenterFactory(factory: SearchUiPresenterFactory): Presenter.Factory = factory
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindSearchUiFactoryFactory(factory: SearchUiFactory): Ui.Factory = factory
}
diff --git a/ui/search/src/main/java/app/tivi/home/search/SearchPresenter.kt b/ui/search/src/commonMain/kotlin/app/tivi/home/search/SearchPresenter.kt
similarity index 100%
rename from ui/search/src/main/java/app/tivi/home/search/SearchPresenter.kt
rename to ui/search/src/commonMain/kotlin/app/tivi/home/search/SearchPresenter.kt
diff --git a/ui/search/src/main/java/app/tivi/home/search/SearchUiState.kt b/ui/search/src/commonMain/kotlin/app/tivi/home/search/SearchUiState.kt
similarity index 100%
rename from ui/search/src/main/java/app/tivi/home/search/SearchUiState.kt
rename to ui/search/src/commonMain/kotlin/app/tivi/home/search/SearchUiState.kt
diff --git a/ui/settings/src/main/java/app/tivi/settings/SettingsActivity.kt b/ui/settings/src/main/kotlin/app/tivi/settings/SettingsActivity.kt
similarity index 100%
rename from ui/settings/src/main/java/app/tivi/settings/SettingsActivity.kt
rename to ui/settings/src/main/kotlin/app/tivi/settings/SettingsActivity.kt
diff --git a/ui/settings/src/main/java/app/tivi/settings/SettingsPreferenceFragment.kt b/ui/settings/src/main/kotlin/app/tivi/settings/SettingsPreferenceFragment.kt
similarity index 100%
rename from ui/settings/src/main/java/app/tivi/settings/SettingsPreferenceFragment.kt
rename to ui/settings/src/main/kotlin/app/tivi/settings/SettingsPreferenceFragment.kt
diff --git a/ui/show/details/build.gradle.kts b/ui/show/details/build.gradle.kts
index df2e1dc476..99cb46b3c3 100644
--- a/ui/show/details/build.gradle.kts
+++ b/ui/show/details/build.gradle.kts
@@ -4,29 +4,31 @@
plugins {
id("app.tivi.android.library")
- id("app.tivi.android.compose")
- id("app.tivi.kotlin.android")
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
}
android {
namespace = "app.tivi.showdetails.details"
}
-dependencies {
- implementation(projects.core.base)
- implementation(projects.domain)
- implementation(projects.common.ui.compose)
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(projects.core.base)
+ implementation(projects.domain)
+ implementation(projects.common.ui.compose)
- api(projects.common.ui.screens)
- api(libs.circuit.foundation)
+ api(projects.common.ui.screens)
+ api(libs.circuit.foundation)
- implementation(libs.compose.foundation.foundation)
- implementation(libs.compose.foundation.layout)
- implementation(libs.compose.material.material)
- implementation(libs.compose.material3.material3)
- implementation(libs.compose.material3.windowsizeclass)
- implementation(libs.compose.animation.animation)
- implementation(libs.compose.ui.tooling)
-
- implementation(libs.coil.compose)
+ implementation(compose.foundation)
+ implementation(compose.material)
+ implementation(compose.material3)
+ implementation(libs.compose.material3.windowsizeclass)
+ implementation(compose.animation)
+ }
+ }
+ }
}
diff --git a/ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetails.kt b/ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetails.kt
similarity index 93%
rename from ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetails.kt
rename to ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetails.kt
index 66ce2baf7d..23a4a98a85 100644
--- a/ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetails.kt
+++ b/ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetails.kt
@@ -11,7 +11,6 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
-import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -27,10 +26,8 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
@@ -39,13 +36,16 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.DismissValue
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Star
-import androidx.compose.material3.DismissValue
+import androidx.compose.material.rememberDismissState
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -54,7 +54,6 @@ import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
-import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
@@ -62,12 +61,10 @@ import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
-import androidx.compose.material3.SwipeToDismiss
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
-import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@@ -88,7 +85,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import app.tivi.common.compose.Layout
import app.tivi.common.compose.LocalTiviTextCreator
-import app.tivi.common.compose.LogCompositions
import app.tivi.common.compose.bodyWidth
import app.tivi.common.compose.gutterSpacer
import app.tivi.common.compose.itemSpacer
@@ -97,7 +93,6 @@ import app.tivi.common.compose.ui.Backdrop
import app.tivi.common.compose.ui.ExpandingText
import app.tivi.common.compose.ui.PosterCard
import app.tivi.common.compose.ui.RefreshButton
-import app.tivi.common.imageloading.TrimTransparentEdgesTransformation
import app.tivi.common.ui.resources.MR
import app.tivi.data.compoundmodels.EpisodeWithSeason
import app.tivi.data.compoundmodels.RelatedShowEntryWithShow
@@ -108,10 +103,10 @@ import app.tivi.data.models.Genre
import app.tivi.data.models.ImageType
import app.tivi.data.models.Season
import app.tivi.data.models.ShowStatus
-import app.tivi.data.models.ShowTmdbImage
import app.tivi.data.models.TiviShow
import app.tivi.data.views.ShowsWatchStats
import app.tivi.screens.ShowDetailsScreen
+import com.moriatsushi.insetsx.navigationBars
import com.slack.circuit.runtime.CircuitContext
import com.slack.circuit.runtime.Screen
import com.slack.circuit.runtime.ui.Ui
@@ -160,6 +155,7 @@ internal fun ShowDetails(
)
}
+@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun ShowDetails(
viewState: ShowDetailsUiState,
@@ -180,16 +176,14 @@ internal fun ShowDetails(
val snackbarHostState = remember { SnackbarHostState() }
val listState = rememberLazyListState()
- val dismissSnackbarState = rememberDismissState(
- confirmValueChange = { value ->
- if (value != DismissValue.Default) {
- snackbarHostState.currentSnackbarData?.dismiss()
- true
- } else {
- false
- }
- },
- )
+ val dismissSnackbarState = rememberDismissState { value ->
+ if (value != DismissValue.Default) {
+ snackbarHostState.currentSnackbarData?.dismiss()
+ true
+ } else {
+ false
+ }
+ }
viewState.message?.let { message ->
LaunchedEffect(message) {
@@ -242,8 +236,6 @@ internal fun ShowDetails(
.exclude(WindowInsets.navigationBars),
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { contentPadding ->
- LogCompositions("ShowDetails")
-
Surface(modifier = Modifier.bodyWidth()) {
ShowDetailsScrollingContent(
show = viewState.show,
@@ -288,8 +280,6 @@ private fun ShowDetailsScrollingContent(
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
- LogCompositions("ShowDetailsScrollingContent")
-
val gutter = Layout.gutter
val bodyMargin = Layout.bodyMargin
@@ -425,7 +415,6 @@ private fun PosterInfoRow(
Row(modifier.padding(horizontal = Layout.bodyMargin)) {
AsyncImage(
model = show.asImageModel(ImageType.POSTER),
- requestBuilder = { crossfade(true) },
contentDescription = stringResource(MR.strings.cd_show_poster, show.title ?: ""),
modifier = Modifier
.weight(1f)
@@ -447,7 +436,6 @@ private fun PosterInfoRow(
private fun NetworkInfoPanel(
networkName: String,
modifier: Modifier = Modifier,
- networkLogoPath: String? = null,
) {
Column(modifier) {
Text(
@@ -457,32 +445,10 @@ private fun NetworkInfoPanel(
Spacer(Modifier.height(4.dp))
- if (networkLogoPath != null) {
- val tmdbImage = remember(networkLogoPath) {
- ShowTmdbImage(path = networkLogoPath, type = ImageType.LOGO, showId = 0)
- }
-
- AsyncImage(
- model = tmdbImage,
- requestBuilder = {
- crossfade(true)
- transformations(TrimTransparentEdgesTransformation)
- },
- contentDescription = stringResource(MR.strings.cd_network_logo),
- modifier = Modifier.sizeIn(maxWidth = 72.dp, maxHeight = 32.dp),
- alignment = Alignment.TopStart,
- contentScale = ContentScale.Fit,
- colorFilter = when {
- isSystemInDarkTheme() -> ColorFilter.tint(LocalContentColor.current)
- else -> null
- },
- )
- } else {
- Text(
- text = networkName,
- style = MaterialTheme.typography.bodyMedium,
- )
- }
+ Text(
+ text = networkName,
+ style = MaterialTheme.typography.bodyMedium,
+ )
}
}
@@ -663,8 +629,6 @@ private fun RelatedShows(
openShowDetails: (showId: Long) -> Unit,
modifier: Modifier = Modifier,
) {
- LogCompositions("RelatedShows")
-
val lazyListState = rememberLazyListState()
val contentPadding = PaddingValues(horizontal = Layout.bodyMargin, vertical = Layout.gutter)
@@ -750,7 +714,6 @@ private fun InfoPanels(
if (show.network != null) {
NetworkInfoPanel(
networkName = show.network!!,
- networkLogoPath = show.networkLogoPath,
modifier = itemMod,
)
}
@@ -976,8 +939,6 @@ private fun ShowDetailsAppBar(
modifier: Modifier = Modifier,
scrollBehavior: TopAppBarScrollBehavior? = null,
) {
- LogCompositions("ShowDetailsAppBar")
-
TopAppBar(
title = { Text(text = title) },
navigationIcon = {
@@ -994,7 +955,6 @@ private fun ShowDetailsAppBar(
refreshing = !isRefreshing,
)
},
- colors = TopAppBarDefaults.topAppBarColors(),
scrollBehavior = scrollBehavior,
modifier = modifier,
)
@@ -1007,8 +967,6 @@ private fun ToggleShowFollowFloatingActionButton(
expanded: Boolean,
modifier: Modifier = Modifier,
) {
- LogCompositions("ToggleShowFollowFloatingActionButton")
-
ExtendedFloatingActionButton(
onClick = onClick,
icon = {
diff --git a/ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetailsComponent.kt b/ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetailsComponent.kt
similarity index 88%
rename from ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetailsComponent.kt
rename to ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetailsComponent.kt
index e723fa9b87..9975642092 100644
--- a/ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetailsComponent.kt
+++ b/ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetailsComponent.kt
@@ -3,7 +3,7 @@
package app.tivi.showdetails.details
-import app.tivi.inject.ApplicationScope
+import app.tivi.inject.ActivityScope
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.ui.Ui
import me.tatarka.inject.annotations.IntoSet
@@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides
interface ShowDetailsComponent {
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindShowDetailsPresenterFactory(factory: ShowDetailsUiPresenterFactory): Presenter.Factory = factory
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindShowDetailsUiFactoryFactory(factory: ShowDetailsUiFactory): Ui.Factory = factory
}
diff --git a/ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetailsPresenter.kt b/ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetailsPresenter.kt
similarity index 100%
rename from ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetailsPresenter.kt
rename to ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetailsPresenter.kt
diff --git a/ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetailsUiState.kt b/ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetailsUiState.kt
similarity index 100%
rename from ui/show/details/src/main/java/app/tivi/showdetails/details/ShowDetailsUiState.kt
rename to ui/show/details/src/commonMain/kotlin/app/tivi/showdetails/details/ShowDetailsUiState.kt
diff --git a/ui/show/seasons/build.gradle.kts b/ui/show/seasons/build.gradle.kts
index 79536295c7..c8564b5d5f 100644
--- a/ui/show/seasons/build.gradle.kts
+++ b/ui/show/seasons/build.gradle.kts
@@ -4,30 +4,32 @@
plugins {
id("app.tivi.android.library")
- id("app.tivi.android.compose")
- id("app.tivi.kotlin.android")
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
}
android {
namespace = "app.tivi.showdetails.seasons"
}
-dependencies {
- implementation(projects.core.base)
- implementation(projects.domain)
- implementation(projects.common.ui.compose)
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(projects.core.base)
+ implementation(projects.domain)
+ implementation(projects.common.ui.compose)
- api(projects.common.ui.screens)
- api(libs.circuit.foundation)
+ api(projects.common.ui.screens)
+ api(libs.circuit.foundation)
- implementation(libs.compose.foundation.foundation)
- implementation(libs.compose.foundation.layout)
- implementation(libs.compose.material.material)
- implementation(libs.compose.material3.material3)
- implementation(libs.compose.material3.windowsizeclass)
- implementation(libs.compose.material.iconsext)
- implementation(libs.compose.animation.animation)
- implementation(libs.compose.ui.tooling)
-
- implementation(libs.coil.compose)
+ implementation(compose.foundation)
+ implementation(compose.material)
+ implementation(compose.material3)
+ implementation(libs.compose.material3.windowsizeclass)
+ implementation(compose.materialIconsExtended)
+ implementation(compose.animation)
+ }
+ }
+ }
}
diff --git a/ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasons.kt b/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasons.kt
similarity index 96%
rename from ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasons.kt
rename to ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasons.kt
index 44d2f3559d..1778e148a7 100644
--- a/ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasons.kt
+++ b/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasons.kt
@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
@@ -23,12 +22,15 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material.DismissValue
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.CloudUpload
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
-import androidx.compose.material3.DismissValue
+import androidx.compose.material.rememberDismissState
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@@ -40,13 +42,11 @@ import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.SwipeToDismiss
import androidx.compose.material3.Tab
import androidx.compose.material3.TabPosition
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
-import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -73,6 +73,7 @@ import app.tivi.data.compoundmodels.SeasonWithEpisodesAndWatches
import app.tivi.data.models.Episode
import app.tivi.data.models.Season
import app.tivi.screens.ShowSeasonsScreen
+import com.moriatsushi.insetsx.statusBars
import com.slack.circuit.runtime.CircuitContext
import com.slack.circuit.runtime.Screen
import com.slack.circuit.runtime.ui.Ui
@@ -113,7 +114,7 @@ internal fun ShowSeasons(
)
}
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Composable
internal fun ShowSeasons(
state: ShowSeasonsUiState,
@@ -125,16 +126,14 @@ internal fun ShowSeasons(
) {
val snackbarHostState = remember { SnackbarHostState() }
- val dismissSnackbarState = rememberDismissState(
- confirmValueChange = { value ->
- if (value != DismissValue.Default) {
- snackbarHostState.currentSnackbarData?.dismiss()
- true
- } else {
- false
- }
- },
- )
+ val dismissSnackbarState = rememberDismissState { value ->
+ if (value != DismissValue.Default) {
+ snackbarHostState.currentSnackbarData?.dismiss()
+ true
+ } else {
+ false
+ }
+ }
state.message?.let { message ->
LaunchedEffect(message) {
diff --git a/ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasonsComponent.kt b/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasonsComponent.kt
similarity index 88%
rename from ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasonsComponent.kt
rename to ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasonsComponent.kt
index aefbad1519..8d56c9b03b 100644
--- a/ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasonsComponent.kt
+++ b/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasonsComponent.kt
@@ -3,7 +3,7 @@
package app.tivi.showdetails.seasons
-import app.tivi.inject.ApplicationScope
+import app.tivi.inject.ActivityScope
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.ui.Ui
import me.tatarka.inject.annotations.IntoSet
@@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides
interface ShowSeasonsComponent {
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindShowSeasonsPresenterFactory(factory: ShowSeasonsUiPresenterFactory): Presenter.Factory = factory
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindShowSeasonsUiFactoryFactory(factory: ShowSeasonsUiFactory): Ui.Factory = factory
}
diff --git a/ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasonsPresenter.kt b/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasonsPresenter.kt
similarity index 100%
rename from ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasonsPresenter.kt
rename to ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasonsPresenter.kt
diff --git a/ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasonsUiState.kt b/ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasonsUiState.kt
similarity index 100%
rename from ui/show/seasons/src/main/java/app/tivi/showdetails/seasons/ShowSeasonsUiState.kt
rename to ui/show/seasons/src/commonMain/kotlin/app/tivi/showdetails/seasons/ShowSeasonsUiState.kt
diff --git a/ui/trending/build.gradle.kts b/ui/trending/build.gradle.kts
index 23697ab406..c1347483bf 100644
--- a/ui/trending/build.gradle.kts
+++ b/ui/trending/build.gradle.kts
@@ -4,27 +4,31 @@
plugins {
id("app.tivi.android.library")
- id("app.tivi.android.compose")
- id("app.tivi.kotlin.android")
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
}
android {
namespace = "app.tivi.home.trending"
}
-dependencies {
- implementation(projects.core.base)
- implementation(projects.domain)
- implementation(projects.common.ui.compose)
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(projects.core.base)
+ implementation(projects.domain)
+ implementation(projects.common.ui.compose)
- api(projects.common.ui.screens)
- api(libs.circuit.foundation)
+ api(projects.common.ui.screens)
+ api(libs.circuit.foundation)
- implementation(libs.paging.compose)
+ implementation(libs.paging.compose)
- implementation(libs.compose.foundation.foundation)
- implementation(libs.compose.foundation.layout)
- implementation(libs.compose.material.material)
- implementation(libs.compose.animation.animation)
- implementation(libs.compose.ui.tooling)
+ implementation(compose.foundation)
+ implementation(compose.material)
+ implementation(compose.animation)
+ }
+ }
+ }
}
diff --git a/ui/trending/src/main/java/app/tivi/home/trending/Trending.kt b/ui/trending/src/commonMain/kotlin/app/tivi/home/trending/Trending.kt
similarity index 100%
rename from ui/trending/src/main/java/app/tivi/home/trending/Trending.kt
rename to ui/trending/src/commonMain/kotlin/app/tivi/home/trending/Trending.kt
diff --git a/ui/trending/src/main/java/app/tivi/home/trending/TrendingShowsComponent.kt b/ui/trending/src/commonMain/kotlin/app/tivi/home/trending/TrendingShowsComponent.kt
similarity index 88%
rename from ui/trending/src/main/java/app/tivi/home/trending/TrendingShowsComponent.kt
rename to ui/trending/src/commonMain/kotlin/app/tivi/home/trending/TrendingShowsComponent.kt
index e91e3b0fcc..538b22b470 100644
--- a/ui/trending/src/main/java/app/tivi/home/trending/TrendingShowsComponent.kt
+++ b/ui/trending/src/commonMain/kotlin/app/tivi/home/trending/TrendingShowsComponent.kt
@@ -3,7 +3,7 @@
package app.tivi.home.trending
-import app.tivi.inject.ApplicationScope
+import app.tivi.inject.ActivityScope
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.ui.Ui
import me.tatarka.inject.annotations.IntoSet
@@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides
interface TrendingShowsComponent {
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindTrendingShowsPresenterFactory(factory: TrendingShowsUiPresenterFactory): Presenter.Factory = factory
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindTrendingShowsUiFactoryFactory(factory: TrendingShowsUiFactory): Ui.Factory = factory
}
diff --git a/ui/trending/src/main/java/app/tivi/home/trending/TrendingShowsPresenter.kt b/ui/trending/src/commonMain/kotlin/app/tivi/home/trending/TrendingShowsPresenter.kt
similarity index 100%
rename from ui/trending/src/main/java/app/tivi/home/trending/TrendingShowsPresenter.kt
rename to ui/trending/src/commonMain/kotlin/app/tivi/home/trending/TrendingShowsPresenter.kt
diff --git a/ui/trending/src/main/java/app/tivi/home/trending/TrendingShowsUiState.kt b/ui/trending/src/commonMain/kotlin/app/tivi/home/trending/TrendingShowsUiState.kt
similarity index 100%
rename from ui/trending/src/main/java/app/tivi/home/trending/TrendingShowsUiState.kt
rename to ui/trending/src/commonMain/kotlin/app/tivi/home/trending/TrendingShowsUiState.kt
diff --git a/ui/upnext/build.gradle.kts b/ui/upnext/build.gradle.kts
index c7ebd8b0e3..56b73b2745 100644
--- a/ui/upnext/build.gradle.kts
+++ b/ui/upnext/build.gradle.kts
@@ -4,36 +4,36 @@
plugins {
id("app.tivi.android.library")
- id("app.tivi.android.compose")
- id("app.tivi.kotlin.android")
+ id("app.tivi.kotlin.multiplatform")
+ alias(libs.plugins.composeMultiplatform)
}
android {
namespace = "app.tivi.home.upnext"
}
-dependencies {
- implementation(projects.core.base)
- implementation(projects.domain)
- implementation(projects.common.ui.compose)
-
- api(projects.common.ui.screens)
- api(projects.common.ui.circuitOverlay)
- api(libs.circuit.foundation)
-
- implementation(libs.paging.compose)
-
- implementation(libs.swipe)
-
- implementation(libs.androidx.core)
-
- implementation(libs.compose.foundation.foundation)
- implementation(libs.compose.foundation.layout)
- implementation(libs.compose.material.material)
- implementation(libs.compose.material.iconsext)
- implementation(libs.compose.material3.material3)
- implementation(libs.compose.animation.animation)
- implementation(libs.compose.ui.tooling)
-
- implementation(libs.coil.compose)
+kotlin {
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation(projects.core.base)
+ implementation(projects.domain)
+ implementation(projects.common.ui.compose)
+
+ api(projects.common.ui.screens)
+ api(projects.common.ui.circuitOverlay)
+ api(libs.circuit.foundation)
+
+ implementation(libs.paging.compose)
+
+ implementation(projects.thirdparty.swipe)
+
+ implementation(compose.foundation)
+ implementation(compose.material)
+ implementation(compose.materialIconsExtended)
+ implementation(compose.material3)
+ implementation(compose.animation)
+ }
+ }
+ }
}
diff --git a/ui/upnext/src/main/java/app/tivi/home/upnext/UpNext.kt b/ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNext.kt
similarity index 94%
rename from ui/upnext/src/main/java/app/tivi/home/upnext/UpNext.kt
rename to ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNext.kt
index 38424b1a31..c0ccc4dd53 100644
--- a/ui/upnext/src/main/java/app/tivi/home/upnext/UpNext.kt
+++ b/ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNext.kt
@@ -22,15 +22,17 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.material.DismissValue
import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
+import androidx.compose.material.rememberDismissState
import androidx.compose.material3.Card
-import androidx.compose.material3.DismissValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
@@ -39,10 +41,8 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.SwipeToDismiss
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.material3.rememberDismissState
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -81,7 +81,7 @@ import app.tivi.data.traktauth.TraktAuthState
import app.tivi.overlays.showInDialog
import app.tivi.screens.AccountScreen
import app.tivi.screens.UpNextScreen
-import coil.compose.AsyncImagePainter
+import com.seiko.imageloader.ImageRequestState
import com.slack.circuit.overlay.LocalOverlayHost
import com.slack.circuit.runtime.CircuitContext
import com.slack.circuit.runtime.Screen
@@ -152,16 +152,14 @@ internal fun UpNext(
) {
val snackbarHostState = remember { SnackbarHostState() }
- val dismissSnackbarState = rememberDismissState(
- confirmValueChange = { value ->
- if (value != DismissValue.Default) {
- snackbarHostState.currentSnackbarData?.dismiss()
- true
- } else {
- false
- }
- },
- )
+ val dismissSnackbarState = rememberDismissState { value ->
+ if (value != DismissValue.Default) {
+ snackbarHostState.currentSnackbarData?.dismiss()
+ true
+ } else {
+ false
+ }
+ }
state.message?.let { message ->
LaunchedEffect(message) {
@@ -383,13 +381,10 @@ private fun UpNextItem(
AsyncImage(
model = model,
- requestBuilder = { crossfade(true) },
onState = { state ->
- if (state is AsyncImagePainter.State.Error) {
- if (state.result.request.data is EpisodeImageModel) {
- // If the episode backdrop request failed, fallback to the show backdrop
- model = show.asImageModel(ImageType.BACKDROP)
- }
+ if (state is ImageRequestState.Failure && model is EpisodeImageModel) {
+ // If the episode backdrop request failed, fallback to the show backdrop
+ model = show.asImageModel(ImageType.BACKDROP)
}
},
contentDescription = null,
diff --git a/ui/upnext/src/main/java/app/tivi/home/upnext/UpNextComponent.kt b/ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNextComponent.kt
similarity index 87%
rename from ui/upnext/src/main/java/app/tivi/home/upnext/UpNextComponent.kt
rename to ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNextComponent.kt
index 2d75b8ac1d..65e05dd6f6 100644
--- a/ui/upnext/src/main/java/app/tivi/home/upnext/UpNextComponent.kt
+++ b/ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNextComponent.kt
@@ -3,7 +3,7 @@
package app.tivi.home.upnext
-import app.tivi.inject.ApplicationScope
+import app.tivi.inject.ActivityScope
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.ui.Ui
import me.tatarka.inject.annotations.IntoSet
@@ -12,11 +12,11 @@ import me.tatarka.inject.annotations.Provides
interface UpNextComponent {
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindUpNextPresenterFactory(factory: UpNextUiPresenterFactory): Presenter.Factory = factory
@IntoSet
@Provides
- @ApplicationScope
+ @ActivityScope
fun bindUpNextUiFactoryFactory(factory: UpNextUiFactory): Ui.Factory = factory
}
diff --git a/ui/upnext/src/main/java/app/tivi/home/upnext/UpNextPresenter.kt b/ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNextPresenter.kt
similarity index 100%
rename from ui/upnext/src/main/java/app/tivi/home/upnext/UpNextPresenter.kt
rename to ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNextPresenter.kt
diff --git a/ui/upnext/src/main/java/app/tivi/home/upnext/UpNextUiState.kt b/ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNextUiState.kt
similarity index 100%
rename from ui/upnext/src/main/java/app/tivi/home/upnext/UpNextUiState.kt
rename to ui/upnext/src/commonMain/kotlin/app/tivi/home/upnext/UpNextUiState.kt