mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-07-26 20:02:14 +00:00
Add Apple multiplatform foundation
This commit is contained in:
parent
a1ccbefc96
commit
45c45b0ce6
20 changed files with 545 additions and 21 deletions
101
.github/workflows/apple.yml
vendored
Normal file
101
.github/workflows/apple.yml
vendored
Normal file
|
|
@ -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
|
||||
45
README.md
45
README.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<img src="app/src/mobile/res/mipmap-nodpi/ic_launcher.png" alt="Fluxa" width="96" />
|
||||
|
||||
# Fluxa
|
||||
# Fluxa for Android
|
||||
|
||||
A fast, native media client for Android phones, tablets, and TV.<br/>
|
||||
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]
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
<!-- MARKDOWN LINKS -->
|
||||
[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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
1
appleApp/.gitignore
vendored
Normal file
1
appleApp/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
FluxaApple.xcodeproj
|
||||
13
appleApp/README.md
Normal file
13
appleApp/README.md
Normal file
|
|
@ -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`.
|
||||
11
appleApp/Shared/FluxaRootView.swift
Normal file
11
appleApp/Shared/FluxaRootView.swift
Normal file
|
|
@ -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) {}
|
||||
}
|
||||
11
appleApp/iOS/FluxaIosApp.swift
Normal file
11
appleApp/iOS/FluxaIosApp.swift
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct FluxaIosApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
FluxaRootView()
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
}
|
||||
29
appleApp/project.yml
Normal file
29
appleApp/project.yml
Normal file
|
|
@ -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
|
||||
11
appleApp/tvOS/FluxaTvosApp.swift
Normal file
11
appleApp/tvOS/FluxaTvosApp.swift
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import FluxaCore
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct FluxaTvosApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
Text(FluxaTvos.shared.homeTitle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<KotlinNativeTarget>()
|
||||
.matching { it.name.startsWith("tvos") }
|
||||
.configureEach {
|
||||
binaries.framework {
|
||||
baseName = "FluxaCore"
|
||||
isStatic = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ actual fun epochMillisNow(): Long = (NSDate().timeIntervalSince1970 * 1000).toLo
|
|||
|
||||
internal actual class SimpleLock actual constructor() {
|
||||
private val lock = NSLock()
|
||||
|
||||
actual fun <T> withLock(block: () -> T): T {
|
||||
lock.lock()
|
||||
try {
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.fluxa.app.common
|
||||
|
||||
object FluxaTvos {
|
||||
fun homeTitle(): String = AppStrings.t(null, "nav.home")
|
||||
}
|
||||
|
|
@ -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<KotlinNativeTarget>()
|
||||
.matching { it.name.startsWith("ios") }
|
||||
.configureEach {
|
||||
binaries.framework {
|
||||
baseName = "FluxaShared"
|
||||
isStatic = true
|
||||
export(project(":core"))
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(project(":core"))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
30
shared/src/iosMain/kotlin/com/fluxa/app/shared/FluxaApple.kt
Normal file
30
shared/src/iosMain/kotlin/com/fluxa/app/shared/FluxaApple.kt
Normal file
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue