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