diff --git a/.github/workflows/apple.yml b/.github/workflows/apple.yml new file mode 100644 index 0000000..8dd2f17 --- /dev/null +++ b/.github/workflows/apple.yml @@ -0,0 +1,101 @@ +name: Apple + +on: + pull_request: + paths: + - ".github/workflows/apple.yml" + - "appleApp/**" + - "build-logic/**" + - "core/**" + - "shared/**" + - "gradle/**" + - "gradle.properties" + - "settings.gradle.kts" + - "build.gradle.kts" + push: + branches: [master] + paths: + - ".github/workflows/apple.yml" + - "appleApp/**" + - "build-logic/**" + - "core/**" + - "shared/**" + - "gradle/**" + - "gradle.properties" + - "settings.gradle.kts" + - "build.gradle.kts" + workflow_dispatch: + +permissions: + contents: read + +jobs: + ios: + name: iOS simulator + runs-on: macos-14 + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - name: Set up XcodeGen + run: | + if ! command -v xcodegen >/dev/null; then + brew install xcodegen + fi + + - name: Verify shared iOS code + run: ./gradlew --no-daemon :shared:iosSimulatorArm64Test + + - name: Generate Apple host project + working-directory: appleApp + run: xcodegen generate + + - name: Build iOS host + run: >- + xcodebuild + -project appleApp/FluxaApple.xcodeproj + -scheme FluxaIos + -sdk iphonesimulator + -destination 'generic/platform=iOS Simulator' + CODE_SIGNING_ALLOWED=NO + build + + tvos: + name: tvOS simulator + runs-on: macos-14 + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - name: Set up XcodeGen + run: | + if ! command -v xcodegen >/dev/null; then + brew install xcodegen + fi + + - name: Build tvOS shared core + run: ./gradlew --no-daemon :core:linkDebugFrameworkTvosSimulatorArm64 + + - name: Generate Apple host project + working-directory: appleApp + run: xcodegen generate + + - name: Build tvOS host + run: >- + xcodebuild + -project appleApp/FluxaApple.xcodeproj + -scheme FluxaTvos + -sdk appletvsimulator + -destination 'generic/platform=tvOS Simulator' + CODE_SIGNING_ALLOWED=NO + build diff --git a/README.md b/README.md index a75fe91..b815489 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Fluxa -# Fluxa +# Fluxa for Android A fast, native media client for Android phones, tablets, and TV.
Browse catalogs, track what you watch, and play anything the Stremio addon ecosystem exposes. @@ -10,10 +10,11 @@ Browse catalogs, track what you watch, and play anything the Stremio addon ecosy [![Contributors][contributors-shield]][contributors-url] [![Forks][forks-shield]][forks-url] [![Stars][stars-shield]][stars-url] -[![Issues][issues-shield]][issues-url] +[![Releases][releases-shield]][releases-url] [![License][license-shield]][license-url] +[![Discord][discord-shield]][discord-url] -[Download](#download) · [Features](#features) · [Building from source](#building-from-source) +[Download](#download) · [Features](#features) · [Building from source](#building-from-source) · [Discord][discord-url] @@ -38,7 +39,7 @@ The Android shell handles all platform I/O — HTTP, Room, ExoPlayer, audio, not ## Download -Grab the latest build from [Releases](https://github.com/KhooLy/Fluxa/releases/latest). Requires Android 8.0+ (API 26). +Grab the latest build from [Releases](https://github.com/FluxaMedia/fluxa/releases/latest). Requires Android 8.0+ (API 26). | Variant | Package | Target | | --- | --- | --- | @@ -56,8 +57,8 @@ Each variant ships per-ABI APKs — pick the one matching your device, or let th ## Building from source ```bash -git clone https://github.com/KhooLy/Fluxa.git -cd Fluxa +git clone https://github.com/FluxaMedia/fluxa.git +cd fluxa ./gradlew :app:assembleMobileDebug # or for TV ./gradlew :app:assembleTvDebug @@ -99,6 +100,10 @@ Kotlin → executes each effect (OkHttp / Room / audio / ...) This keeps `fluxa_core` fully portable — the same crate compiles for Android (JNI), desktop (native Rust), and future targets without any platform-specific code inside Rust. +## Platform roadmap + +Fluxa for Android ships phone, tablet, and Android TV builds today. The UI is being migrated to Compose Multiplatform so iOS can use the same application interface, while tvOS uses the same shared domain logic with native system integration. + ## Stack [Kotlin](https://kotlinlang.org/) · [Jetpack Compose](https://developer.android.com/compose) · [Rust](https://www.rust-lang.org/) · [AndroidX Media3 / ExoPlayer](https://developer.android.com/media/media3) · [MPV](https://mpv.io/) · [librqbit](https://github.com/ikatson/rqbit) · [UniFFI](https://mozilla.github.io/uniffi-rs/) · [Hilt](https://dagger.dev/hilt/) · [Room](https://developer.android.com/training/data-storage/room) · [OkHttp](https://square.github.io/okhttp/) + [Retrofit](https://square.github.io/retrofit/) · [Coil](https://coil-kt.github.io/coil/) @@ -107,19 +112,25 @@ This keeps `fluxa_core` fully portable — the same crate compiles for Android ( **Legal** — Fluxa is a client-side interface for user-installed Stremio addons. It does not host, serve, or distribute any media content. All streams come from third-party addons chosen by the user. Fluxa is not affiliated with any addon developer, repository, or content provider. Users are responsible for ensuring they have the right to access what they stream. +## Community + +Questions, bug reports, feature requests, or just want to hang out — join the [Fluxa Discord][discord-url]. + ## Related projects -- [Fluxa Desktop](https://github.com/FluxaMedia/fluxa-desktop) — the desktop counterpart to this app +- [Fluxa Desktop](https://github.com/FluxaMedia/fluxa-desktop) — the Windows, macOS, and Linux counterpart - [fluxa-core](https://github.com/FluxaMedia/fluxa-core) — the shared Rust library powering both -[contributors-shield]: https://img.shields.io/github/contributors/KhooLy/Fluxa.svg?style=for-the-badge -[contributors-url]: https://github.com/KhooLy/Fluxa/graphs/contributors -[forks-shield]: https://img.shields.io/github/forks/KhooLy/Fluxa.svg?style=for-the-badge -[forks-url]: https://github.com/KhooLy/Fluxa/network/members -[stars-shield]: https://img.shields.io/github/stars/KhooLy/Fluxa.svg?style=for-the-badge -[stars-url]: https://github.com/KhooLy/Fluxa/stargazers -[issues-shield]: https://img.shields.io/github/issues/KhooLy/Fluxa.svg?style=for-the-badge -[issues-url]: https://github.com/KhooLy/Fluxa/issues -[license-shield]: https://img.shields.io/github/license/KhooLy/Fluxa.svg?style=for-the-badge -[license-url]: https://github.com/KhooLy/Fluxa/blob/master/LICENSE +[contributors-shield]: https://img.shields.io/github/contributors/FluxaMedia/fluxa.svg?style=for-the-badge +[contributors-url]: https://github.com/FluxaMedia/fluxa/graphs/contributors +[forks-shield]: https://img.shields.io/github/forks/FluxaMedia/fluxa.svg?style=for-the-badge +[forks-url]: https://github.com/FluxaMedia/fluxa/network/members +[stars-shield]: https://img.shields.io/github/stars/FluxaMedia/fluxa.svg?style=for-the-badge +[stars-url]: https://github.com/FluxaMedia/fluxa/stargazers +[releases-shield]: https://img.shields.io/github/v/release/FluxaMedia/fluxa.svg?style=for-the-badge +[releases-url]: https://github.com/FluxaMedia/fluxa/releases/latest +[license-shield]: https://img.shields.io/github/license/FluxaMedia/fluxa.svg?style=for-the-badge +[license-url]: https://github.com/FluxaMedia/fluxa/blob/master/LICENSE +[discord-shield]: https://img.shields.io/badge/Discord-Join-5865F2.svg?style=for-the-badge&logo=discord&logoColor=white +[discord-url]: https://discord.gg/wan9FeDEfe diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/mobile/MobileExploreScreen.kt b/app/src/main/java/com/fluxa/app/ui/catalog/mobile/MobileExploreScreen.kt index 4994e13..324f94e 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/mobile/MobileExploreScreen.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/mobile/MobileExploreScreen.kt @@ -259,7 +259,8 @@ fun MobileExploreScreen( ) { _, movie -> val source = discoverSourceFor(movie, resultSources) MobileExplorePosterCard( - movie = movie, + title = movie.name, + artworkUrl = movie.poster, onClick = { onMovieClick(movie, source?.transportUrl, source?.type) } ) } @@ -269,7 +270,7 @@ fun MobileExploreScreen( } @Composable -private fun MobileExplorePosterCard( +private fun LegacyMobileExplorePosterCard( movie: Meta, onClick: () -> Unit ) { @@ -302,7 +303,7 @@ private fun MobileExplorePosterCard( } @Composable -private fun MobileExplorePosterCardSkeleton() { +private fun LegacyMobileExplorePosterCardSkeleton() { val transition = rememberInfiniteTransition(label = "explore-skeleton") val alpha by transition.animateFloat( initialValue = 0.05f, diff --git a/app/src/mobile/java/com/fluxa/app/ui/catalog/MobileHomeCards.kt b/app/src/mobile/java/com/fluxa/app/ui/catalog/MobileHomeCards.kt index 9d92f1d..507e408 100644 --- a/app/src/mobile/java/com/fluxa/app/ui/catalog/MobileHomeCards.kt +++ b/app/src/mobile/java/com/fluxa/app/ui/catalog/MobileHomeCards.kt @@ -59,7 +59,7 @@ internal fun collectionFolderLayout(reason: String?, fallbackLayout: String): St internal const val HOME_CATALOG_LOAD_MORE_THRESHOLD = 5 @Composable -internal fun HomeCatalogCard( +private fun LegacyHomeCatalogCard( artwork: String?, width: androidx.compose.ui.unit.Dp, imageHeight: androidx.compose.ui.unit.Dp, diff --git a/appleApp/.gitignore b/appleApp/.gitignore new file mode 100644 index 0000000..bb8de85 --- /dev/null +++ b/appleApp/.gitignore @@ -0,0 +1 @@ +FluxaApple.xcodeproj diff --git a/appleApp/README.md b/appleApp/README.md new file mode 100644 index 0000000..fb2314a --- /dev/null +++ b/appleApp/README.md @@ -0,0 +1,13 @@ +# Apple hosts + +`FluxaIos` is a SwiftUI host for the Compose Multiplatform `FluxaShared` framework. `FluxaTvos` is a native SwiftUI host backed by the Kotlin Multiplatform `FluxaCore` framework. + +On macOS, install XcodeGen and generate the project: + +```bash +cd appleApp +xcodegen generate +open FluxaApple.xcodeproj +``` + +The Xcode build phase invokes Gradle with the active Xcode SDK and architecture, then embeds the matching framework. Both targets bundle the shared English and Turkish i18n files from `core/src/androidMain/assets/i18n`. diff --git a/appleApp/Shared/FluxaRootView.swift b/appleApp/Shared/FluxaRootView.swift new file mode 100644 index 0000000..e189e55 --- /dev/null +++ b/appleApp/Shared/FluxaRootView.swift @@ -0,0 +1,11 @@ +import FluxaShared +import SwiftUI +import UIKit + +struct FluxaRootView: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> UIViewController { + FluxaApple.shared.rootViewController() + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} diff --git a/appleApp/iOS/FluxaIosApp.swift b/appleApp/iOS/FluxaIosApp.swift new file mode 100644 index 0000000..ce8a5c7 --- /dev/null +++ b/appleApp/iOS/FluxaIosApp.swift @@ -0,0 +1,11 @@ +import SwiftUI + +@main +struct FluxaIosApp: App { + var body: some Scene { + WindowGroup { + FluxaRootView() + .ignoresSafeArea() + } + } +} diff --git a/appleApp/project.yml b/appleApp/project.yml new file mode 100644 index 0000000..87a67a8 --- /dev/null +++ b/appleApp/project.yml @@ -0,0 +1,29 @@ +name: FluxaApple +options: + deploymentTarget: + iOS: "16.0" + tvOS: "16.0" +targets: + FluxaIos: + type: application + platform: iOS + sources: + - path: Shared + - path: iOS + resources: + - path: ../core/src/androidMain/assets/i18n + preBuildScripts: + - name: Build FluxaShared + script: ../gradlew :shared:embedAndSignAppleFrameworkForXcode + basedOnDependencyAnalysis: false + FluxaTvos: + type: application + platform: tvOS + sources: + - path: tvOS + resources: + - path: ../core/src/androidMain/assets/i18n + preBuildScripts: + - name: Build FluxaCore + script: ../gradlew :core:embedAndSignAppleFrameworkForXcode + basedOnDependencyAnalysis: false diff --git a/appleApp/tvOS/FluxaTvosApp.swift b/appleApp/tvOS/FluxaTvosApp.swift new file mode 100644 index 0000000..39bb501 --- /dev/null +++ b/appleApp/tvOS/FluxaTvosApp.swift @@ -0,0 +1,11 @@ +import FluxaCore +import SwiftUI + +@main +struct FluxaTvosApp: App { + var body: some Scene { + WindowGroup { + Text(FluxaTvos.shared.homeTitle()) + } + } +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 1f3e05d..1f34cc0 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + plugins { alias(libs.plugins.fluxa.kmp.library) } @@ -7,6 +9,19 @@ android { } kotlin { + tvosArm64() + tvosSimulatorArm64() + + targets + .withType() + .matching { it.name.startsWith("tvos") } + .configureEach { + binaries.framework { + baseName = "FluxaCore" + isStatic = true + } + } + sourceSets { commonMain.dependencies { implementation(libs.kotlinx.coroutines.core) diff --git a/core/src/iosMain/kotlin/com/fluxa/app/common/AppStrings.ios.kt b/core/src/appleMain/kotlin/com/fluxa/app/common/AppStrings.apple.kt similarity index 100% rename from core/src/iosMain/kotlin/com/fluxa/app/common/AppStrings.ios.kt rename to core/src/appleMain/kotlin/com/fluxa/app/common/AppStrings.apple.kt diff --git a/core/src/iosMain/kotlin/com/fluxa/app/common/LocalizedCalendarText.ios.kt b/core/src/appleMain/kotlin/com/fluxa/app/common/LocalizedCalendarText.apple.kt similarity index 100% rename from core/src/iosMain/kotlin/com/fluxa/app/common/LocalizedCalendarText.ios.kt rename to core/src/appleMain/kotlin/com/fluxa/app/common/LocalizedCalendarText.apple.kt diff --git a/core/src/iosMain/kotlin/com/fluxa/app/common/PlatformClock.ios.kt b/core/src/appleMain/kotlin/com/fluxa/app/common/PlatformClock.apple.kt similarity index 99% rename from core/src/iosMain/kotlin/com/fluxa/app/common/PlatformClock.ios.kt rename to core/src/appleMain/kotlin/com/fluxa/app/common/PlatformClock.apple.kt index 4050bf4..f93c09b 100644 --- a/core/src/iosMain/kotlin/com/fluxa/app/common/PlatformClock.ios.kt +++ b/core/src/appleMain/kotlin/com/fluxa/app/common/PlatformClock.apple.kt @@ -8,6 +8,7 @@ actual fun epochMillisNow(): Long = (NSDate().timeIntervalSince1970 * 1000).toLo internal actual class SimpleLock actual constructor() { private val lock = NSLock() + actual fun withLock(block: () -> T): T { lock.lock() try { diff --git a/core/src/tvosMain/kotlin/com/fluxa/app/common/FluxaTvos.kt b/core/src/tvosMain/kotlin/com/fluxa/app/common/FluxaTvos.kt new file mode 100644 index 0000000..9b5cc44 --- /dev/null +++ b/core/src/tvosMain/kotlin/com/fluxa/app/common/FluxaTvos.kt @@ -0,0 +1,5 @@ +package com.fluxa.app.common + +object FluxaTvos { + fun homeTitle(): String = AppStrings.t(null, "nav.home") +} diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index b83030e..a44ce85 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + plugins { alias(libs.plugins.fluxa.kmp.compose) } @@ -7,6 +9,17 @@ android { } kotlin { + targets + .withType() + .matching { it.name.startsWith("ios") } + .configureEach { + binaries.framework { + baseName = "FluxaShared" + isStatic = true + export(project(":core")) + } + } + sourceSets { commonMain.dependencies { implementation(project(":core")) diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/HomeCatalogCard.kt b/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/HomeCatalogCard.kt new file mode 100644 index 0000000..82bc074 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/HomeCatalogCard.kt @@ -0,0 +1,164 @@ +package com.fluxa.app.ui.catalog + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.AsyncImage +import coil3.compose.LocalPlatformContext +import coil3.request.ImageRequest +import coil3.request.crossfade + +@Composable +fun HomeCatalogCard( + artwork: String?, + width: Dp, + imageHeight: Dp, + showTitle: Boolean, + title: String, + secondary: String, + showProgressBar: Boolean, + progress: Float, + isUpNext: Boolean, + upNextLabel: String, + topTenRank: Int?, + coverEmoji: String?, + name: String, + onClick: () -> Unit, + onLongClick: (() -> Unit)? +) { + val context = LocalPlatformContext.current + val request = remember(context, artwork) { + ImageRequest.Builder(context) + .data(artwork) + .crossfade(false) + .memoryCacheKey(artwork?.let { "home-catalog:$it" }) + .diskCacheKey(artwork) + .build() + } + val clickModifier = if (onLongClick != null) { + Modifier.combinedClickable(interactionSource = null, indication = null, onClick = onClick, onLongClick = onLongClick) + } else { + Modifier.clickable(interactionSource = null, indication = null, onClick = onClick) + } + Column( + modifier = Modifier + .width(width) + .height(imageHeight + if (showTitle) 42.dp else 0.dp) + .then(clickModifier) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(imageHeight) + .background(Color.White.copy(alpha = 0.05f)), + contentAlignment = Alignment.Center + ) { + if (!artwork.isNullOrBlank()) { + AsyncImage( + model = request, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + } else { + androidx.compose.material3.Text( + text = coverEmoji?.takeIf { it.isNotBlank() } ?: name.take(1).uppercase(), + color = Color.White.copy(alpha = if (coverEmoji.isNullOrBlank()) 0.2f else 0.82f), + fontSize = if (coverEmoji.isNullOrBlank()) 42.sp else 36.sp, + fontWeight = FontWeight.Black + ) + } + if (showProgressBar) { + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 6.dp) + .height(4.dp) + .clip(RoundedCornerShape(999.dp)) + .background(Color.White.copy(alpha = 0.2f)) + ) { + Box( + modifier = Modifier + .fillMaxWidth(progress) + .fillMaxHeight() + .background(FluxaColors.progressFill) + ) + } + } + if (isUpNext) { + androidx.compose.material3.Text( + text = upNextLabel, + color = Color.White, + fontSize = 10.sp, + fontWeight = FontWeight.Black, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(8.dp) + .background(Color.Black.copy(alpha = 0.72f)) + .padding(horizontal = 8.dp, vertical = 5.dp) + ) + } + if (topTenRank != null) { + androidx.compose.material3.Text( + text = topTenRank.toString(), + color = Color.White, + fontSize = 18.sp, + fontWeight = FontWeight.Black, + modifier = Modifier + .align(Alignment.TopStart) + .padding(7.dp) + .background(Color.Black.copy(alpha = 0.66f), RoundedCornerShape(6.dp)) + .padding(horizontal = 7.dp, vertical = 3.dp) + ) + } + } + if (showTitle) { + androidx.compose.material3.Text( + text = title, + color = Color.White, + fontSize = 12.sp, + lineHeight = 13.sp, + fontWeight = FontWeight.Black, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 4.dp) + ) + if (secondary.isNotBlank()) { + androidx.compose.material3.Text( + text = secondary, + color = Color.White.copy(alpha = 0.58f), + fontSize = 10.sp, + lineHeight = 11.sp, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 0.dp) + ) + } + } + } +} diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/MobileExplorePosterCard.kt b/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/MobileExplorePosterCard.kt new file mode 100644 index 0000000..d7276dd --- /dev/null +++ b/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/MobileExplorePosterCard.kt @@ -0,0 +1,54 @@ +package com.fluxa.app.ui.catalog + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.AsyncImage + +@Composable +fun MobileExplorePosterCard( + title: String, + artworkUrl: String?, + onClick: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + ) { + AsyncImage( + model = artworkUrl, + contentDescription = title, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(2f / 3f) + .clip(RoundedCornerShape(10.dp)) + .background(Color.White.copy(alpha = 0.06f)), + contentScale = ContentScale.Crop + ) + Spacer(Modifier.height(6.dp)) + androidx.compose.material3.Text( + text = title, + color = Color.White, + fontSize = 14.sp, + fontWeight = FontWeight.Black, + lineHeight = 16.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } +} diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/MobileExplorePosterCardSkeleton.kt b/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/MobileExplorePosterCardSkeleton.kt new file mode 100644 index 0000000..75887d8 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/MobileExplorePosterCardSkeleton.kt @@ -0,0 +1,53 @@ +package com.fluxa.app.ui.catalog + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun MobileExplorePosterCardSkeleton() { + val transition = rememberInfiniteTransition(label = "explore-skeleton") + val alpha by transition.animateFloat( + initialValue = 0.05f, + targetValue = 0.12f, + animationSpec = infiniteRepeatable( + animation = tween(FluxaDimensions.AnimDuration.ambientColor, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "explore-skeleton-alpha" + ) + Column(modifier = Modifier.fillMaxWidth()) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(2f / 3f) + .clip(RoundedCornerShape(10.dp)) + .background(Color.White.copy(alpha = alpha)) + ) + Spacer(Modifier.height(6.dp)) + Box( + modifier = Modifier + .fillMaxWidth(0.7f) + .height(14.dp) + .clip(RoundedCornerShape(4.dp)) + .background(Color.White.copy(alpha = alpha)) + ) + } +} diff --git a/shared/src/iosMain/kotlin/com/fluxa/app/shared/FluxaApple.kt b/shared/src/iosMain/kotlin/com/fluxa/app/shared/FluxaApple.kt new file mode 100644 index 0000000..c69e913 --- /dev/null +++ b/shared/src/iosMain/kotlin/com/fluxa/app/shared/FluxaApple.kt @@ -0,0 +1,30 @@ +package com.fluxa.app.shared + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.ComposeUIViewController +import com.fluxa.app.common.AppStrings +import platform.UIKit.UIViewController + +object FluxaApple { + fun rootViewController(): UIViewController = ComposeUIViewController { + FluxaApp() + } +} + +@Composable +private fun FluxaApp() { + MaterialTheme { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(AppStrings.t(null, "nav.home")) + } + } +}