diff --git a/codecov.yml b/codecov.yml index c1f1774..58439c0 100644 --- a/codecov.yml +++ b/codecov.yml @@ -22,3 +22,5 @@ ignore: - "composeApp/src/androidMain/kotlin/com/nacchofer31/randomboxd/Platform.android.kt" - "composeApp/src/androidMain/kotlin/com/nacchofer31/randomboxd/core/data/OnboardingPreferences.android.kt" - "composeApp/src/androidMain/kotlin/com/nacchofer31/randomboxd/AndroidPlatform.kt" + - "composeApp/src/androidMain/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/ShareRepositoryImplAndroid.kt" + - "composeApp/src/iosMain/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/ShareRepositoryImplIos.kt" diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 87ca331..5c8dbd8 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -268,6 +268,8 @@ val fileFilter = // Native platform implementations (require device context) "com/nacchofer31/randomboxd/random_film/data/repository_impl/InAppReviewRepositoryImplAndroid*", "com/nacchofer31/randomboxd/random_film/data/repository_impl/InAppReviewRepositoryImplIos*", + "com/nacchofer31/randomboxd/random_film/data/repository_impl/ShareRepositoryImplAndroid*", + "com/nacchofer31/randomboxd/random_film/data/repository_impl/ShareRepositoryImplIos*", ) tasks.register("jacocoTestReport", JacocoReport::class) { diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmPosterTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmPosterTest.kt index 41a4bea..e4b189b 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmPosterTest.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/FilmPosterTest.kt @@ -56,6 +56,7 @@ class FilmPosterTest { releaseYear = "2020", onClick = {}, onRerollClick = {}, + onShareClick = {}, ) } @@ -82,6 +83,7 @@ class FilmPosterTest { releaseYear = "2020", onClick = {}, onRerollClick = {}, + onShareClick = {}, ) } @@ -108,6 +110,7 @@ class FilmPosterTest { releaseYear = "2010", onClick = { clicked = true }, onRerollClick = {}, + onShareClick = {}, ) } @@ -134,6 +137,7 @@ class FilmPosterTest { releaseYear = "2020", onClick = {}, onRerollClick = {}, + onShareClick = {}, numberOfResults = 0, ) } @@ -159,6 +163,7 @@ class FilmPosterTest { releaseYear = "2020", onClick = {}, onRerollClick = {}, + onShareClick = {}, numberOfResults = 42, ) } diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt index 8e8ad50..6b7632f 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/feature/random_film/presentation/RandomFilmScreenTest.kt @@ -281,6 +281,35 @@ class RandomFilmScreenTest { assert(rerollClicked) } + @Test + fun share_button_opens_share_card_dialog() { + val bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888) + setImageLoader( + FakeImageLoaderEngine + .Builder() + .default(bitmap.asImage()) + .build(), + ) + + composeTestRule.setContent { + val mutableUserNamesFlow = MutableStateFlow>(emptyList()) + RandomFilmScreen( + userNameList = mutableUserNamesFlow, + resultFilm = + Film( + slug = "test-slug", + name = "test-name", + releaseYear = 2000, + imageUrl = "test-image-url", + ), + ) { } + } + + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("test-share-button").performClick() + composeTestRule.onNodeWithText("The dice has spoken... Today's pick is...").assertIsDisplayed() + } + @Test fun film_poster_click_triggers_film_clicked_action() { val bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888) diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/history/presentation/HistoryCardTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/history/presentation/HistoryCardTest.kt index e4e8935..37009a5 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/history/presentation/HistoryCardTest.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/history/presentation/HistoryCardTest.kt @@ -69,6 +69,7 @@ class HistoryCardTest { pick: FilmPick, onPosterClick: (String) -> Unit = {}, onFavoriteToggle: () -> Unit = {}, + onShareClick: () -> Unit = {}, ) { composeTestRule.setContent { HistoryCard( @@ -76,6 +77,7 @@ class HistoryCardTest { metaText = "Today · 10:30", onPosterClick = onPosterClick, onFavoriteToggle = onFavoriteToggle, + onShareClick = onShareClick, ) } } @@ -186,4 +188,17 @@ class HistoryCardTest { assertTrue(clickedSlug == "inception") } + + @Test + fun history_card_share_button_triggers_callback() { + var shared = false + setCardContent( + samplePick(), + onShareClick = { shared = true }, + ) + + composeTestRule.onNodeWithContentDescription("Share").performClick() + + assertTrue(shared) + } } diff --git a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/history/presentation/HistoryScreenTest.kt b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/history/presentation/HistoryScreenTest.kt index efc23d1..343a76a 100644 --- a/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/history/presentation/HistoryScreenTest.kt +++ b/composeApp/src/androidInstrumentedTest/kotlin/com/randomboxd/history/presentation/HistoryScreenTest.kt @@ -1,5 +1,6 @@ package com.randomboxd.history.presentation +import android.graphics.Bitmap import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -10,12 +11,19 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import coil3.ImageLoader +import coil3.SingletonImageLoader +import coil3.annotation.DelicateCoilApi +import coil3.asImage +import coil3.test.FakeImageLoaderEngine import com.nacchofer31.randomboxd.history.domain.model.FilmPick import com.nacchofer31.randomboxd.history.presentation.HistoryScreen import com.nacchofer31.randomboxd.history.presentation.HistoryScreenRoot import com.nacchofer31.randomboxd.history.presentation.viewmodel.HistoryAction import com.nacchofer31.randomboxd.random_film.domain.model.FilmGenre import com.nacchofer31.randomboxd.random_film.domain.model.FilmSearchMode +import org.junit.After import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test @@ -29,6 +37,20 @@ class HistoryScreenTest { @get:Rule val composeTestRule = createComposeRule() + private val context get() = InstrumentationRegistry.getInstrumentation().targetContext + + @After + @OptIn(DelicateCoilApi::class) + fun resetImageLoader() { + SingletonImageLoader.reset() + } + + private fun setImageLoader(engine: FakeImageLoaderEngine) { + SingletonImageLoader.setSafe { + ImageLoader.Builder(context).components { add(engine) }.build() + } + } + private fun samplePick( id: Int, filmName: String, @@ -218,4 +240,31 @@ class HistoryScreenTest { composeTestRule.onNodeWithContentDescription("Clear history").assertIsDisplayed() composeTestRule.onNodeWithText("History").assertIsDisplayed() } + + @Test + fun history_screen_share_button_opens_share_dialog() { + val bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888) + setImageLoader( + FakeImageLoaderEngine + .Builder() + .default(bitmap.asImage()) + .build(), + ) + + composeTestRule.setContent { + HistoryScreen( + picks = listOf(samplePick(id = 1, filmName = "Inception")), + showClearConfirmDialog = false, + isFavoritesOnly = false, + onBackClick = {}, + onPosterClick = {}, + onAction = {}, + isLoading = false, + ) + } + + composeTestRule.waitForIdle() + composeTestRule.onNodeWithContentDescription("Share").performClick() + composeTestRule.onNodeWithText("The dice has spoken... Today's pick is...").assertIsDisplayed() + } } diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 89fe349..eee23cc 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -21,5 +21,14 @@ + + + \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/nacchofer31/randomboxd/di/Modules.android.kt b/composeApp/src/androidMain/kotlin/com/nacchofer31/randomboxd/di/Modules.android.kt index 0c3da1b..5044d66 100644 --- a/composeApp/src/androidMain/kotlin/com/nacchofer31/randomboxd/di/Modules.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nacchofer31/randomboxd/di/Modules.android.kt @@ -4,7 +4,9 @@ import com.nacchofer31.randomboxd.core.data.OnboardingPreferences import com.nacchofer31.randomboxd.core.data.RandomBoxdDatabase import com.nacchofer31.randomboxd.database.getRandomBoxdDatabase import com.nacchofer31.randomboxd.random_film.data.repository_impl.InAppReviewRepositoryImplAndroid +import com.nacchofer31.randomboxd.random_film.data.repository_impl.ShareRepositoryImplAndroid import com.nacchofer31.randomboxd.random_film.domain.repository.InAppReviewRepository +import com.nacchofer31.randomboxd.random_film.domain.repository.ShareRepository import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.okhttp.OkHttp import org.koin.core.module.Module @@ -18,4 +20,5 @@ actual val platformModule: Module single { getRandomBoxdDatabase(get()) } single { OnboardingPreferences(get()) } single { InAppReviewRepositoryImplAndroid() } bind InAppReviewRepository::class + single { ShareRepositoryImplAndroid(get()) } bind ShareRepository::class } diff --git a/composeApp/src/androidMain/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/ShareRepositoryImplAndroid.kt b/composeApp/src/androidMain/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/ShareRepositoryImplAndroid.kt new file mode 100644 index 0000000..b0de89a --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/ShareRepositoryImplAndroid.kt @@ -0,0 +1,48 @@ +package com.nacchofer31.randomboxd.random_film.data.repository_impl + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.core.content.FileProvider +import com.nacchofer31.randomboxd.random_film.domain.repository.ShareRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File + +class ShareRepositoryImplAndroid( + private val context: Context, +) : ShareRepository { + override suspend fun shareImage( + image: ImageBitmap, + fileName: String, + ) { + val bitmap = image.asAndroidBitmap() + val safeFileName = sanitizeFileName(fileName) + val uri = + withContext(Dispatchers.IO) { + val dir = File(context.cacheDir, "share") + dir.mkdirs() + val file = File(dir, "$safeFileName.png") + file.outputStream().use { stream -> + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) + } + FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) + } + val intent = + Intent(Intent.ACTION_SEND).apply { + type = "image/png" + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + val chooser = Intent.createChooser(intent, null).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } + context.startActivity(chooser) + } + + private fun sanitizeFileName(fileName: String): String { + val slug = fileName.substringAfterLast('/').trimEnd('/') + val sanitized = slug.replace(Regex("[^A-Za-z0-9._-]"), "_").trim('_') + return sanitized.ifBlank { "randomboxd" } + } +} diff --git a/composeApp/src/androidMain/res/xml/file_paths.xml b/composeApp/src/androidMain/res/xml/file_paths.xml new file mode 100644 index 0000000..e2db02d --- /dev/null +++ b/composeApp/src/androidMain/res/xml/file_paths.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/drawable/google_play_store_badge_en.xml b/composeApp/src/commonMain/composeResources/drawable/google_play_store_badge_en.xml new file mode 100644 index 0000000..2520b1d --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/google_play_store_badge_en.xml @@ -0,0 +1,33 @@ + + + + + + + + + + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-ar/strings.xml b/composeApp/src/commonMain/composeResources/values-ar/strings.xml index ab6a98e..6e6e171 100644 --- a/composeApp/src/commonMain/composeResources/values-ar/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-ar/strings.xml @@ -78,6 +78,8 @@ جاري رمي النرد... جاري البحث عن فيلمك العشوائي إعادة الرمي + مشاركة + تحدثت النرد... اختيار اليوم هو... السجل اختياراتك العشوائية لا توجد أفلام مختارة بعد diff --git a/composeApp/src/commonMain/composeResources/values-ca/strings.xml b/composeApp/src/commonMain/composeResources/values-ca/strings.xml index d5164ef..102feb4 100644 --- a/composeApp/src/commonMain/composeResources/values-ca/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-ca/strings.xml @@ -78,6 +78,8 @@ Llançant els daus... Trobar la teva pel·lícula aleatòria Tornar a tirar + Compartir + Els daus han parlat... L'elecció d'avui és... Historial Les teves seleccions aleatòries Encara no has seleccionat cap pel·lícula diff --git a/composeApp/src/commonMain/composeResources/values-de/strings.xml b/composeApp/src/commonMain/composeResources/values-de/strings.xml index 21a174a..13a9bbc 100644 --- a/composeApp/src/commonMain/composeResources/values-de/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-de/strings.xml @@ -78,6 +78,8 @@ Würfel rollen... Finde deinen zufälligen Film Neu würfeln + Teilen + Die Würfel sind gefallen... Die heutige Auswahl ist... Verlauf Deine Zufallsauswahlen Noch keine Filme ausgewählt diff --git a/composeApp/src/commonMain/composeResources/values-es/strings.xml b/composeApp/src/commonMain/composeResources/values-es/strings.xml index f51bebc..6086c24 100644 --- a/composeApp/src/commonMain/composeResources/values-es/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-es/strings.xml @@ -78,6 +78,8 @@ Lanzando los dados... Encontrando tu película aleatoria Tirar de nuevo + Compartir + Los dados han hablado... La elección de hoy es... Historial Tus selecciones aleatorias Aún no has seleccionado películas diff --git a/composeApp/src/commonMain/composeResources/values-fr/strings.xml b/composeApp/src/commonMain/composeResources/values-fr/strings.xml index 01dc5e7..97a3bcc 100644 --- a/composeApp/src/commonMain/composeResources/values-fr/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-fr/strings.xml @@ -78,6 +78,8 @@ Lancement des dés... Trouver votre film aléatoire Relancer + Partager + Les dés ont parlé... Le choix du jour est... Historique Tes sélections aléatoires Aucun film sélectionné pour le moment diff --git a/composeApp/src/commonMain/composeResources/values-gl/strings.xml b/composeApp/src/commonMain/composeResources/values-gl/strings.xml index e1c52a6..29a147d 100644 --- a/composeApp/src/commonMain/composeResources/values-gl/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-gl/strings.xml @@ -78,6 +78,8 @@ Lanzando os dados... Atopando o teu filme aleatorio Tirar de novo + Compartir + Os dados falaron... A escolla de hoxe é... Historial As túas seleccións aleatorias Aínda non seleccionaches películas diff --git a/composeApp/src/commonMain/composeResources/values-hi/strings.xml b/composeApp/src/commonMain/composeResources/values-hi/strings.xml index b572afc..634f339 100644 --- a/composeApp/src/commonMain/composeResources/values-hi/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-hi/strings.xml @@ -78,6 +78,8 @@ पासा फेंक रहे हैं... आपकी यादृच्छिक फिल्म खोज रहे हैं फिर से फेंकें + साझा करें + पासे बोल चुके हैं... आज की पसंद है... इतिहास आपके रैंडम चयन अभी तक कोई फ़िल्म चयनित नहीं diff --git a/composeApp/src/commonMain/composeResources/values-it/strings.xml b/composeApp/src/commonMain/composeResources/values-it/strings.xml index 2c08e08..72223e9 100644 --- a/composeApp/src/commonMain/composeResources/values-it/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-it/strings.xml @@ -78,6 +78,8 @@ Lanciando i dadi... Trovare il tuo film casuale Rilancia + Condividi + I dadi hanno parlato... La scelta di oggi è... Cronologia Le tue selezioni casuali Nessun film selezionato finora diff --git a/composeApp/src/commonMain/composeResources/values-ja/strings.xml b/composeApp/src/commonMain/composeResources/values-ja/strings.xml index 73d8315..d13fdf7 100644 --- a/composeApp/src/commonMain/composeResources/values-ja/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-ja/strings.xml @@ -78,6 +78,8 @@ サイコロを振っています... ランダム映画を探しています もう一度振る + 共有 + サイコロは語った…今日の一作は… 履歴 あなたのランダム選択 まだ選択された映画はありません diff --git a/composeApp/src/commonMain/composeResources/values-pt/strings.xml b/composeApp/src/commonMain/composeResources/values-pt/strings.xml index 7b727f3..2e979d6 100644 --- a/composeApp/src/commonMain/composeResources/values-pt/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-pt/strings.xml @@ -78,6 +78,8 @@ Lançando os dados... Encontrando seu filme aleatório Rolar novamente + Compartilhar + Os dados falaram... A escolha de hoje é... Histórico Suas seleções aleatórias Nenhum filme selecionado ainda diff --git a/composeApp/src/commonMain/composeResources/values-ru/strings.xml b/composeApp/src/commonMain/composeResources/values-ru/strings.xml index 40f376d..3ac4c43 100644 --- a/composeApp/src/commonMain/composeResources/values-ru/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-ru/strings.xml @@ -78,6 +78,8 @@ Бросаем кубики... Ищем ваш случайный фильм Перебросить + Поделиться + Кости брошены... Выбор дня —... История Ваши случайные выборы Пока нет выбранных фильмов diff --git a/composeApp/src/commonMain/composeResources/values-zh/strings.xml b/composeApp/src/commonMain/composeResources/values-zh/strings.xml index 55ca27c..a109d46 100644 --- a/composeApp/src/commonMain/composeResources/values-zh/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-zh/strings.xml @@ -78,6 +78,8 @@ 掷骰子中... 正在寻找您的随机电影 重新掷骰 + 分享 + 骰子已经落下……今天的选片是…… 历史记录 你的随机选择 还没有选择的电影 diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 4ad2d4f..bb1cd61 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -78,6 +78,8 @@ Rolling the dice... Finding your random movie Reroll + Share + The dice has spoken... Today's pick is... History Your random picks No movies picked yet diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/history/presentation/HistoryScreen.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/history/presentation/HistoryScreen.kt index a51a948..e982d6b 100644 --- a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/history/presentation/HistoryScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/history/presentation/HistoryScreen.kt @@ -17,8 +17,12 @@ import androidx.compose.material3.TextButton 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.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nacchofer31.randomboxd.core.presentation.RandomBoxdColors @@ -32,7 +36,9 @@ import com.nacchofer31.randomboxd.history.presentation.components.TimestampDispl import com.nacchofer31.randomboxd.history.presentation.components.formatPickTimestamp import com.nacchofer31.randomboxd.history.presentation.viewmodel.HistoryAction import com.nacchofer31.randomboxd.history.presentation.viewmodel.HistoryViewModel +import com.nacchofer31.randomboxd.random_film.domain.model.Film import com.nacchofer31.randomboxd.random_film.presentation.components.LoadingOrPrompt +import com.nacchofer31.randomboxd.random_film.presentation.components.ShareFilmCardDialog import kotlinx.datetime.TimeZone import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel @@ -64,6 +70,7 @@ fun HistoryScreenRoot( onPosterClick = onPosterClick, onAction = viewModel::onAction, isLoading = state.isLoading, + onShareImage = viewModel::shareImage, ) } @@ -76,8 +83,10 @@ fun HistoryScreen( onPosterClick: (String) -> Unit, onAction: (HistoryAction) -> Unit, isLoading: Boolean, + onShareImage: (ImageBitmap, String) -> Unit = { _, _ -> }, ) { val listState = rememberLazyListState() + var pickToShare by remember { mutableStateOf(null) } LaunchedEffect(isFavoritesOnly) { listState.scrollToItem(0) } @@ -109,6 +118,20 @@ fun HistoryScreen( ) } + pickToShare?.let { pick -> + ShareFilmCardDialog( + film = + Film( + slug = pick.filmSlug, + imageUrl = pick.posterUrl, + releaseYear = pick.releaseYear, + name = pick.filmName, + ), + onShare = onShareImage, + onDismiss = { pickToShare = null }, + ) + } + Scaffold( topBar = { HistoryHeader( @@ -154,6 +177,7 @@ fun HistoryScreen( metaText = metaText, onPosterClick = onPosterClick, onFavoriteToggle = { onAction(HistoryAction.ToggleFavorite(pick.id)) }, + onShareClick = { pickToShare = pick }, modifier = Modifier.fillMaxWidth(), ) } diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/history/presentation/components/HistoryCard.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/history/presentation/components/HistoryCard.kt index 73a4afe..d5eb0ae 100644 --- a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/history/presentation/components/HistoryCard.kt +++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/history/presentation/components/HistoryCard.kt @@ -9,10 +9,12 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.outlined.FavoriteBorder import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -42,6 +44,7 @@ fun HistoryCard( metaText: String, onPosterClick: (String) -> Unit, onFavoriteToggle: () -> Unit, + onShareClick: () -> Unit, modifier: Modifier = Modifier, ) { Card( @@ -141,32 +144,54 @@ fun HistoryCard( } } - Box( - modifier = - Modifier - .size(36.dp) - .background( - color = - if (pick.isFavorite) { - RandomBoxdColors.TagGreenColor - } else { - RandomBoxdColors.ElevatedBackgroundColor - }, - shape = RoundedCornerShape(18.dp), - ).clickable(onClick = onFavoriteToggle), - contentAlignment = Alignment.Center, + Column( + verticalArrangement = Arrangement.spacedBy(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { - Icon( - imageVector = Icons.Outlined.FavoriteBorder, - contentDescription = if (pick.isFavorite) "Unfavorite" else "Favorite", - tint = - if (pick.isFavorite) { - RandomBoxdColors.GreenAccent - } else { - RandomBoxdColors.TextMuted - }, - modifier = Modifier.size(16.dp), - ) + Box( + modifier = + Modifier + .size(36.dp) + .background( + color = + if (pick.isFavorite) { + RandomBoxdColors.TagGreenColor + } else { + RandomBoxdColors.ElevatedBackgroundColor + }, + shape = RoundedCornerShape(18.dp), + ).clickable(onClick = onFavoriteToggle), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Outlined.FavoriteBorder, + contentDescription = if (pick.isFavorite) "Unfavorite" else "Favorite", + tint = + if (pick.isFavorite) { + RandomBoxdColors.GreenAccent + } else { + RandomBoxdColors.TextMuted + }, + modifier = Modifier.size(16.dp), + ) + } + Box( + modifier = + Modifier + .size(36.dp) + .background( + color = RandomBoxdColors.ElevatedBackgroundColor, + shape = RoundedCornerShape(18.dp), + ).clickable(onClick = onShareClick), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Filled.Share, + contentDescription = "Share", + tint = RandomBoxdColors.BlueAccent, + modifier = Modifier.size(16.dp), + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/history/presentation/viewmodel/HistoryViewModel.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/history/presentation/viewmodel/HistoryViewModel.kt index 9b0f963..16c6535 100644 --- a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/history/presentation/viewmodel/HistoryViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/history/presentation/viewmodel/HistoryViewModel.kt @@ -1,9 +1,11 @@ package com.nacchofer31.randomboxd.history.presentation.viewmodel +import androidx.compose.ui.graphics.ImageBitmap import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nacchofer31.randomboxd.history.domain.model.FilmPick import com.nacchofer31.randomboxd.history.domain.repository.FilmHistoryRepository +import com.nacchofer31.randomboxd.random_film.domain.repository.ShareRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.flow.MutableStateFlow @@ -20,6 +22,7 @@ import kotlinx.coroutines.withContext class HistoryViewModel( private val repository: FilmHistoryRepository, + private val shareRepository: ShareRepository, ) : ViewModel() { private val _state = MutableStateFlow(HistoryState(isLoading = true)) val state: StateFlow = _state.asStateFlow() @@ -40,6 +43,15 @@ class HistoryViewModel( } } + fun shareImage( + image: ImageBitmap, + fileName: String, + ) { + viewModelScope.launch { + shareRepository.shareImage(image, fileName) + } + } + private fun toggleFavorite(pickId: Int) { val pick = historyPicks.value.find { it.id == pickId } ?: return viewModelScope.launch { diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/domain/repository/ShareRepository.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/domain/repository/ShareRepository.kt new file mode 100644 index 0000000..ca639b5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/domain/repository/ShareRepository.kt @@ -0,0 +1,10 @@ +package com.nacchofer31.randomboxd.random_film.domain.repository + +import androidx.compose.ui.graphics.ImageBitmap + +interface ShareRepository { + suspend fun shareImage( + image: ImageBitmap, + fileName: String, + ) +} diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/RandomFilmScreen.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/RandomFilmScreen.kt index 9c5c49e..958ebe4 100644 --- a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/RandomFilmScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/RandomFilmScreen.kt @@ -15,10 +15,13 @@ import androidx.compose.material3.Scaffold 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.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.dp @@ -36,6 +39,7 @@ import com.nacchofer31.randomboxd.random_film.presentation.components.FilmHeader import com.nacchofer31.randomboxd.random_film.presentation.components.GenreFilterBottomSheet import com.nacchofer31.randomboxd.random_film.presentation.components.LoadingOrPrompt import com.nacchofer31.randomboxd.random_film.presentation.components.RandomFilmInfoView +import com.nacchofer31.randomboxd.random_film.presentation.components.ShareFilmCardDialog import com.nacchofer31.randomboxd.random_film.presentation.components.UnionIntersectionSwitch import com.nacchofer31.randomboxd.random_film.presentation.components.UserNameTagListView import com.nacchofer31.randomboxd.random_film.presentation.viewmodel.RandomFilmAction @@ -105,6 +109,7 @@ fun RandomFilmScreenRoot( numberOfResults = numberOfResults, userNameList = viewModel.userNameList, onAction = onAction, + onShareImage = viewModel::shareImage, ) } @@ -120,9 +125,11 @@ fun RandomFilmScreen( showGenreBottomSheet: Boolean = false, numberOfResults: Int = 0, userNameList: StateFlow> = MutableStateFlow(emptyList()), + onShareImage: (ImageBitmap, String) -> Unit = { _, _ -> }, onAction: (RandomFilmAction) -> Unit = {}, ) { val focusManager = LocalFocusManager.current + var showShareDialog by remember { mutableStateOf(false) } if (showGenreBottomSheet) { GenreFilterBottomSheet( @@ -132,6 +139,14 @@ fun RandomFilmScreen( ) } + resultFilm?.takeIf { showShareDialog }?.let { + ShareFilmCardDialog( + film = it, + onShare = onShareImage, + onDismiss = { showShareDialog = false }, + ) + } + Scaffold( topBar = { FilmHeader( @@ -168,7 +183,12 @@ fun RandomFilmScreen( FilmErrorView(it) } resultFilm?.takeIf { !isLoading }?.let { - FilmDisplay(it, onAction, numberOfResults) + FilmDisplay( + it, + onAction, + numberOfResults, + onShareClick = { showShareDialog = true }, + ) } ?: LoadingOrPrompt(isLoading) if (resultError == null && resultFilm == null && !isLoading) { diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/FilmDisplay.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/FilmDisplay.kt index 74d6cb6..c8f5127 100644 --- a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/FilmDisplay.kt +++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/FilmDisplay.kt @@ -15,6 +15,7 @@ internal fun FilmDisplay( film: Film, onAction: (RandomFilmAction) -> Unit, numberOfResults: Int = 0, + onShareClick: () -> Unit = {}, ) = Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(vertical = 20.dp).testTag("test-film-display"), @@ -29,6 +30,7 @@ internal fun FilmDisplay( onRerollClick = { onAction(RandomFilmAction.OnRerollClicked) }, + onShareClick = onShareClick, numberOfResults = numberOfResults, ) } diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/FilmPoster.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/FilmPoster.kt index 730350d..00d2406 100644 --- a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/FilmPoster.kt +++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/FilmPoster.kt @@ -4,8 +4,10 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize @@ -50,6 +52,7 @@ fun FilmPoster( releaseYear: String, onClick: () -> Unit, onRerollClick: () -> Unit, + onShareClick: () -> Unit, numberOfResults: Int = 0, ) { var imageLoadResult by remember { @@ -117,7 +120,7 @@ fun FilmPoster( modifier = Modifier .fillMaxWidth() - .aspectRatio(280f / 360f), + .aspectRatio(2f / 3f), ) { Box( modifier = @@ -186,9 +189,17 @@ fun FilmPoster( textAlign = TextAlign.Center, ) if (numberOfResults > 1) { - RerollButton( - onClick = onRerollClick, - ) + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + ShareButton(onClick = onShareClick) + RerollButton( + onClick = onRerollClick, + ) + } + } else { + ShareButton(onClick = onShareClick) } } } diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/ShareButton.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/ShareButton.kt new file mode 100644 index 0000000..a4424ea --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/ShareButton.kt @@ -0,0 +1,54 @@ +package com.nacchofer31.randomboxd.random_film.presentation.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.nacchofer31.randomboxd.core.presentation.RandomBoxdColors +import org.jetbrains.compose.resources.stringResource +import randomboxd.composeapp.generated.resources.Res +import randomboxd.composeapp.generated.resources.share + +@Composable +internal fun ShareButton(onClick: () -> Unit) = + Surface( + onClick = { onClick() }, + modifier = + Modifier + .padding(top = 5.dp) + .testTag("test-share-button"), + shape = RoundedCornerShape(100), + color = RandomBoxdColors.CardBackground, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 20.dp, vertical = 6.dp), + ) { + Icon( + imageVector = Icons.Filled.Share, + contentDescription = null, + tint = RandomBoxdColors.BlueAccent, + modifier = Modifier.size(20.dp), + ) + Text( + text = stringResource(Res.string.share), + color = RandomBoxdColors.White, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + ) + } + } diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/ShareCard.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/ShareCard.kt new file mode 100644 index 0000000..1603c59 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/ShareCard.kt @@ -0,0 +1,198 @@ +package com.nacchofer31.randomboxd.random_film.presentation.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.AsyncImage +import com.nacchofer31.randomboxd.core.presentation.RandomBoxdColors +import com.nacchofer31.randomboxd.random_film.domain.model.Film +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import randomboxd.composeapp.generated.resources.Res +import randomboxd.composeapp.generated.resources.google_play_store_badge_en +import randomboxd.composeapp.generated.resources.onboarding_welcome_subtitle +import randomboxd.composeapp.generated.resources.random_boxd_logo +import randomboxd.composeapp.generated.resources.share_card_tagline + +@Composable +fun ShareCard( + film: Film, + modifier: Modifier = Modifier, + onPosterLoaded: () -> Unit = {}, +) { + Box( + modifier = + modifier + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .background( + Brush.radialGradient( + colors = + listOf( + RandomBoxdColors.BackgroundColor, + RandomBoxdColors.BackgroundDarkColor, + ), + center = Offset(0.5f, 0.35f), + radius = 1000f, + ), + ).drawBehind { + drawDicePattern() + }, + ) { + Column( + modifier = Modifier.fillMaxWidth().padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = stringResource(Res.string.share_card_tagline), + color = RandomBoxdColors.BackgroundLightColor, + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = 1.sp, + textAlign = TextAlign.Center, + ) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Box( + modifier = + Modifier + .fillMaxWidth(0.8f) + .aspectRatio(2f / 3f) + .clip(RoundedCornerShape(16.dp)) + .background(RandomBoxdColors.BackgroundColor), + ) { + AsyncImage( + model = film.imageUrl, + contentDescription = film.name, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop, + onSuccess = { onPosterLoaded() }, + ) + } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = film.name, + color = RandomBoxdColors.White, + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + ) + Text( + text = film.releaseYear?.toString() ?: "-", + color = RandomBoxdColors.BackgroundLightColor, + fontSize = 16.sp, + textAlign = TextAlign.Center, + ) + } + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(3.dp), + modifier = + Modifier + .padding(horizontal = 14.dp, vertical = 7.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Image( + painter = painterResource(Res.drawable.random_boxd_logo), + contentDescription = null, + modifier = Modifier.size(40.dp), + ) + Text( + text = "RandomBoxd", + color = RandomBoxdColors.White, + fontSize = 16.sp, + fontWeight = FontWeight.ExtraBold, + ) + } + Text( + text = stringResource(Res.string.onboarding_welcome_subtitle), + color = RandomBoxdColors.BackgroundLightColor, + fontSize = 10.sp, + textAlign = TextAlign.Center, + ) + } + Image( + painter = painterResource(Res.drawable.google_play_store_badge_en), + contentDescription = null, + modifier = Modifier.fillMaxWidth(0.42f).aspectRatio(180f / 53.333f), + ) + } + } + } +} + +private fun DrawScope.drawDicePattern() { + val cell = 34.dp.toPx() + val die = 20.dp.toPx() + val pip = 1.5.dp.toPx() + val gap = 4.dp.toPx() + val accentColors = + listOf( + RandomBoxdColors.GreenAccent, + RandomBoxdColors.OrangeAccent, + RandomBoxdColors.BlueAccent, + ) + var row = 0 + var y = -die + while (y < size.height) { + var x = -die + (if (row % 2 == 0) 0f else cell / 2) + while (x < size.width) { + val color = accentColors[(row + (x / cell).toInt()) % accentColors.size].copy(alpha = 0.05f) + drawRoundRect( + color = color, + topLeft = Offset(x, y), + size = Size(die, die), + cornerRadius = CornerRadius(die * 0.18f), + ) + drawCircle(color, pip, Offset(x + gap, y + gap)) + drawCircle(color, pip, Offset(x + die - gap, y + gap)) + drawCircle(color, pip, Offset(x + gap, y + die - gap)) + drawCircle(color, pip, Offset(x + die - gap, y + die - gap)) + drawCircle(color, pip, Offset(x + die / 2, y + die / 2)) + x += cell + } + y += cell * 0.87f + row++ + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/ShareFilmCardDialog.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/ShareFilmCardDialog.kt new file mode 100644 index 0000000..fe0d378 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/components/ShareFilmCardDialog.kt @@ -0,0 +1,175 @@ +package com.nacchofer31.randomboxd.random_film.presentation.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +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.runtime.withFrameNanos +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.rememberGraphicsLayer +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import com.nacchofer31.randomboxd.core.presentation.RandomBoxdColors +import com.nacchofer31.randomboxd.random_film.domain.model.Film +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.stringResource +import randomboxd.composeapp.generated.resources.Res +import randomboxd.composeapp.generated.resources.history_clear_cancel +import randomboxd.composeapp.generated.resources.share + +@Composable +fun ShareFilmCardDialog( + film: Film, + onShare: (ImageBitmap, String) -> Unit, + onDismiss: () -> Unit, +) { + val graphicsLayer = rememberGraphicsLayer() + val scope = rememberCoroutineScope() + var sharing by remember { mutableStateOf(false) } + var posterLoaded by remember { mutableStateOf(false) } + var capturedBitmap by remember { mutableStateOf(null) } + val dismissDialog = { + sharing = false + onDismiss() + } + + LaunchedEffect(posterLoaded) { + if (posterLoaded) { + while (graphicsLayer.size.width <= 0 || graphicsLayer.size.height <= 0) { + withFrameNanos { } + } + capturedBitmap = graphicsLayer.toImageBitmap() + } + } + + Dialog(onDismissRequest = dismissDialog) { + Column( + modifier = + Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Box( + modifier = + Modifier + .fillMaxWidth() + .drawWithContent { + graphicsLayer.record { + this@drawWithContent.drawContent() + } + drawLayer(graphicsLayer) + }, + contentAlignment = Alignment.Center, + ) { + ShareCard( + film = film, + onPosterLoaded = { posterLoaded = true }, + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Surface( + onClick = dismissDialog, + shape = RoundedCornerShape(100), + color = RandomBoxdColors.ElevatedBackgroundColor, + modifier = Modifier.weight(1f), + ) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 12.dp), + ) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = null, + tint = RandomBoxdColors.White, + modifier = Modifier.size(18.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(Res.string.history_clear_cancel), + color = RandomBoxdColors.White, + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + ) + } + } + Surface( + onClick = { + val bitmap = capturedBitmap + if (!sharing && bitmap != null) { + sharing = true + scope.launch { + onShare(bitmap, film.slug) + dismissDialog() + } + } + }, + shape = RoundedCornerShape(100), + color = RandomBoxdColors.GreenAccent, + modifier = + Modifier + .weight(1f) + .testTag("test-share-dialog-button"), + ) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 12.dp), + ) { + Icon( + imageVector = Icons.Filled.Share, + contentDescription = null, + tint = RandomBoxdColors.BackgroundDarkColor, + modifier = Modifier.size(18.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(Res.string.share), + color = RandomBoxdColors.BackgroundDarkColor, + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + ) + } + } + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/viewmodel/RandomFilmViewModel.kt b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/viewmodel/RandomFilmViewModel.kt index 03c3486..97d0f42 100644 --- a/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/viewmodel/RandomFilmViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/nacchofer31/randomboxd/random_film/presentation/viewmodel/RandomFilmViewModel.kt @@ -1,5 +1,6 @@ package com.nacchofer31.randomboxd.random_film.presentation.viewmodel +import androidx.compose.ui.graphics.ImageBitmap import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nacchofer31.randomboxd.core.domain.DispatcherProvider @@ -12,6 +13,7 @@ import com.nacchofer31.randomboxd.random_film.domain.model.FilmSearchMode import com.nacchofer31.randomboxd.random_film.domain.model.UserName import com.nacchofer31.randomboxd.random_film.domain.repository.InAppReviewRepository import com.nacchofer31.randomboxd.random_film.domain.repository.RandomFilmRepository +import com.nacchofer31.randomboxd.random_film.domain.repository.ShareRepository import com.nacchofer31.randomboxd.random_film.domain.repository.UserNameRepository import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow @@ -37,6 +39,7 @@ class RandomFilmViewModel( private val dispatchers: DispatcherProvider, private val inAppReviewRepository: InAppReviewRepository, private val historyRepository: FilmHistoryRepository, + private val shareRepository: ShareRepository, ) : ViewModel() { private val actions = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) @@ -310,4 +313,13 @@ class RandomFilmViewModel( } } } + + fun shareImage( + image: ImageBitmap, + fileName: String, + ) { + viewModelScope.launch { + shareRepository.shareImage(image, fileName) + } + } } diff --git a/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt index 11b79b8..044b186 100644 --- a/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt +++ b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/feature/random_film/presentation/RandomFilmViewModelTest.kt @@ -9,6 +9,7 @@ import com.nacchofer31.randomboxd.random_film.domain.model.FilmGenre import com.nacchofer31.randomboxd.random_film.domain.model.FilmSearchMode import com.nacchofer31.randomboxd.random_film.domain.repository.InAppReviewRepository import com.nacchofer31.randomboxd.random_film.domain.repository.RandomFilmRepository +import com.nacchofer31.randomboxd.random_film.domain.repository.ShareRepository import com.nacchofer31.randomboxd.random_film.domain.repository.UserNameRepository import com.nacchofer31.randomboxd.random_film.presentation.viewmodel.RandomFilmAction import com.nacchofer31.randomboxd.random_film.presentation.viewmodel.RandomFilmViewModel @@ -42,6 +43,8 @@ class RandomFilmViewModelTest : TestsWithMocks() { @Mock lateinit var historyRepository: FilmHistoryRepository + @Mock lateinit var shareRepository: ShareRepository + private val testFilm = Film( slug = "test-film", @@ -55,6 +58,7 @@ class RandomFilmViewModelTest : TestsWithMocks() { userNameRepository = mocker.mock() inAppReviewRepository = mocker.mock() historyRepository = mocker.mock() + shareRepository = mocker.mock() mocker.every { userNameRepository.getAllUserNames() } returns flow { emit(emptyList()) } @@ -74,7 +78,7 @@ class RandomFilmViewModelTest : TestsWithMocks() { } private fun createViewModel() { - viewModel = RandomFilmViewModel(repository, userNameRepository, testDispatchers, inAppReviewRepository, historyRepository) + viewModel = RandomFilmViewModel(repository, userNameRepository, testDispatchers, inAppReviewRepository, historyRepository, shareRepository) } @Test @@ -615,8 +619,15 @@ class RandomFilmViewModelTest : TestsWithMocks() { object : InAppReviewRepository { override suspend fun requestInAppReview() {} } + val fakeShareRepository = + object : ShareRepository { + override suspend fun shareImage( + image: androidx.compose.ui.graphics.ImageBitmap, + fileName: String, + ) {} + } - viewModel = RandomFilmViewModel(fakeRepository, fakeUserNameRepository, testDispatchers, fakeInAppReviewRepository, historyRepository) + viewModel = RandomFilmViewModel(fakeRepository, fakeUserNameRepository, testDispatchers, fakeInAppReviewRepository, historyRepository, fakeShareRepository) viewModel.onAction(RandomFilmAction.OnUserNameChanged("user")) viewModel.state.test { diff --git a/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/history/presentation/viewmodel/HistoryViewModelTest.kt b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/history/presentation/viewmodel/HistoryViewModelTest.kt index cc9eb2a..45ad14d 100644 --- a/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/history/presentation/viewmodel/HistoryViewModelTest.kt +++ b/composeApp/src/commonTest/kotlin/com/nacchofer31/randomboxd/history/presentation/viewmodel/HistoryViewModelTest.kt @@ -4,6 +4,7 @@ import app.cash.turbine.test import com.nacchofer31.randomboxd.history.domain.model.FilmPick import com.nacchofer31.randomboxd.history.domain.repository.FilmHistoryRepository import com.nacchofer31.randomboxd.random_film.domain.model.FilmSearchMode +import com.nacchofer31.randomboxd.random_film.domain.repository.ShareRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf @@ -27,8 +28,11 @@ import kotlin.time.Instant class HistoryViewModelTest : TestsWithMocks() { @Mock lateinit var repository: FilmHistoryRepository + @Mock lateinit var shareRepository: ShareRepository + private lateinit var viewModel: HistoryViewModel + @OptIn(ExperimentalCoroutinesApi::class) private val testDispatcher = UnconfinedTestDispatcher() @OptIn(ExperimentalCoroutinesApi::class) @@ -72,10 +76,11 @@ class HistoryViewModelTest : TestsWithMocks() { override fun setUpMocks() { repository = mocker.mock() + shareRepository = mocker.mock() } private fun createViewModel() { - viewModel = HistoryViewModel(repository) + viewModel = HistoryViewModel(repository, shareRepository) } @Test diff --git a/composeApp/src/iosMain/kotlin/com/nacchofer31/randomboxd/di/Modules.ios.kt b/composeApp/src/iosMain/kotlin/com/nacchofer31/randomboxd/di/Modules.ios.kt index a993b3b..0ba9319 100644 --- a/composeApp/src/iosMain/kotlin/com/nacchofer31/randomboxd/di/Modules.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nacchofer31/randomboxd/di/Modules.ios.kt @@ -4,7 +4,9 @@ import com.nacchofer31.randomboxd.core.data.OnboardingPreferences import com.nacchofer31.randomboxd.core.data.RandomBoxdDatabase import com.nacchofer31.randomboxd.database.getRandomBoxdDatabase import com.nacchofer31.randomboxd.random_film.data.repository_impl.InAppReviewRepositoryImplIos +import com.nacchofer31.randomboxd.random_film.data.repository_impl.ShareRepositoryImplIos import com.nacchofer31.randomboxd.random_film.domain.repository.InAppReviewRepository +import com.nacchofer31.randomboxd.random_film.domain.repository.ShareRepository import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.darwin.Darwin import org.koin.core.module.Module @@ -18,4 +20,5 @@ actual val platformModule: Module single { getRandomBoxdDatabase() } single { OnboardingPreferences() } single { InAppReviewRepositoryImplIos() } bind InAppReviewRepository::class + single { ShareRepositoryImplIos() } bind ShareRepository::class } diff --git a/composeApp/src/iosMain/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/ShareRepositoryImplIos.kt b/composeApp/src/iosMain/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/ShareRepositoryImplIos.kt new file mode 100644 index 0000000..d12a726 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/com/nacchofer31/randomboxd/random_film/data/repository_impl/ShareRepositoryImplIos.kt @@ -0,0 +1,47 @@ +package com.nacchofer31.randomboxd.random_film.data.repository_impl + +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asSkiaBitmap +import com.nacchofer31.randomboxd.random_film.domain.repository.ShareRepository +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.usePinned +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.skia.EncodedImageFormat +import org.jetbrains.skia.Image +import platform.Foundation.NSData +import platform.Foundation.create +import platform.UIKit.UIActivityViewController +import platform.UIKit.UIApplication +import platform.UIKit.UIImage +import platform.UIKit.UIWindowScene + +class ShareRepositoryImplIos : ShareRepository { + override suspend fun shareImage( + image: ImageBitmap, + fileName: String, + ) { + withContext(Dispatchers.Main) { + val skiaImage = Image.makeFromBitmap(image.asSkiaBitmap()) + val encoded = skiaImage.encodeToData(EncodedImageFormat.PNG) ?: return@withContext + val uiImage = UIImage.imageWithData(encoded.toNSData()) ?: return@withContext + val activityViewController = + UIActivityViewController( + activityItems = listOf(uiImage), + applicationActivities = null, + ) + val windowScene = UIApplication.sharedApplication.connectedScenes.anyObject() as? UIWindowScene + val rootViewController = + windowScene?.windows?.firstOrNull()?.rootViewController + ?: UIApplication.sharedApplication.keyWindow?.rootViewController + rootViewController?.presentViewController(activityViewController, animated = true, completion = null) + } + } + + private fun org.jetbrains.skia.Data.toNSData(): NSData { + val bytes = this.bytes + return bytes.usePinned { pinned -> + NSData.create(bytes = pinned.addressOf(0), length = bytes.size.toULong()) + } + } +} diff --git a/randomboxd.pen b/randomboxd.pen index ca57c78..e352d84 100644 --- a/randomboxd.pen +++ b/randomboxd.pen @@ -2030,47 +2030,104 @@ }, { "type": "frame", - "id": "sZ2BH", - "name": "rerollButton", - "fill": "$accent-green", - "cornerRadius": 100, - "effect": { - "type": "shadow", - "shadowType": "outer", - "color": "#00E05440", - "offset": { - "x": 0, - "y": 4 - }, - "blur": 20 - }, - "gap": 10, - "padding": [ - 12, - 20 - ], + "id": "coH8m", + "name": "actionRow", + "width": "fill_container", + "gap": 12, "justifyContent": "center", "alignItems": "center", "children": [ { - "type": "icon", - "id": "LoJRn", - "name": "shuffleIcon", - "width": 20, - "height": 20, - "icon": "shuffle", - "library": "lucide", - "fill": "#14181C" + "type": "frame", + "id": "hx1MZ", + "name": "shareButton", + "fill": "$bg-card", + "cornerRadius": 100, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000040", + "offset": { + "x": 0, + "y": 4 + }, + "blur": 16 + }, + "gap": 8, + "padding": [ + 12, + 18 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "lPTPx", + "name": "shareIcon", + "width": 18, + "height": 18, + "icon": "share-2", + "library": "lucide", + "fill": "$accent-teal" + }, + { + "type": "text", + "id": "WmGPY", + "name": "shareText", + "fill": "$text-primary", + "content": "Share", + "fontFamily": "Plus Jakarta Sans", + "fontSize": 16, + "fontWeight": "600" + } + ] }, { - "type": "text", - "id": "XwlNm", - "name": "rerollText", - "fill": "#14181C", - "content": "Reroll", - "fontFamily": "Plus Jakarta Sans", - "fontSize": 16, - "fontWeight": "700" + "type": "frame", + "id": "sZ2BH", + "name": "rerollButton", + "fill": "$accent-green", + "cornerRadius": 100, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00E05440", + "offset": { + "x": 0, + "y": 4 + }, + "blur": 20 + }, + "gap": 10, + "padding": [ + 12, + 20 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "LoJRn", + "name": "shuffleIcon", + "width": 20, + "height": 20, + "icon": "shuffle", + "library": "lucide", + "fill": "#14181C" + }, + { + "type": "text", + "id": "XwlNm", + "name": "rerollText", + "fill": "#14181C", + "content": "Reroll", + "fontFamily": "Plus Jakarta Sans", + "fontSize": 16, + "fontWeight": "700" + } + ] } ] } @@ -7165,7 +7222,7 @@ }, { "type": "frame", - "id": "H4rRi", + "id": "vxa3k", "x": 3410, "y": 0, "name": "History - Film History", @@ -7177,7 +7234,7 @@ "children": [ { "type": "frame", - "id": "GolTR", + "id": "ozHCa", "name": "statusBar", "width": "fill_container", "height": 44, @@ -7190,7 +7247,7 @@ "children": [ { "type": "text", - "id": "r1pO6a", + "id": "DQsoO", "name": "statusTime", "fill": "$text-primary", "content": "9:41", @@ -7200,14 +7257,14 @@ }, { "type": "frame", - "id": "UXo66", + "id": "W4GBXJ", "name": "statusIcons", "gap": 6, "alignItems": "center", "children": [ { "type": "icon", - "id": "d1DmeM", + "id": "rQxqf", "name": "signalIcon", "width": 16, "height": 16, @@ -7217,7 +7274,7 @@ }, { "type": "icon", - "id": "IMbsy", + "id": "c3g5Df", "name": "wifiIcon", "width": 16, "height": 16, @@ -7227,7 +7284,7 @@ }, { "type": "icon", - "id": "E9raAZ", + "id": "uQnE0", "name": "batteryIcon", "width": 20, "height": 16, @@ -7241,7 +7298,7 @@ }, { "type": "frame", - "id": "lbyqn", + "id": "G0Pfdl", "name": "historyHeader", "width": "fill_container", "height": 56, @@ -7254,14 +7311,14 @@ "children": [ { "type": "frame", - "id": "L3UMyu", + "id": "uG4jU", "name": "headerLeft", "gap": 12, "alignItems": "center", "children": [ { "type": "frame", - "id": "ZLcH9", + "id": "jsZi2", "name": "backBtn", "width": 36, "height": 36, @@ -7272,7 +7329,7 @@ "children": [ { "type": "icon", - "id": "XUrNf", + "id": "uv9mB", "name": "backIcon", "width": 18, "height": 18, @@ -7284,14 +7341,14 @@ }, { "type": "frame", - "id": "b7fDGX", + "id": "hQtx7", "name": "titleGroup", "layout": "vertical", "gap": 1, "children": [ { "type": "text", - "id": "Z2QjK", + "id": "UtqMi", "name": "historyTitle", "fill": "$text-primary", "content": "History", @@ -7301,7 +7358,7 @@ }, { "type": "text", - "id": "h2u1F4", + "id": "w43xuG", "name": "historySubtitle", "fill": "$text-muted", "content": "Your random picks", @@ -7315,7 +7372,7 @@ }, { "type": "frame", - "id": "N6aot", + "id": "rMRfQ", "name": "clearHistoryBtn", "width": 36, "height": 36, @@ -7326,7 +7383,7 @@ "children": [ { "type": "icon", - "id": "P6aapS", + "id": "RR7u2", "name": "clearIcon", "width": 16, "height": 16, @@ -7340,7 +7397,7 @@ }, { "type": "frame", - "id": "uSOmz", + "id": "b3UONE", "name": "historyMain", "width": "fill_container", "height": "fill_container", @@ -7355,7 +7412,7 @@ "children": [ { "type": "frame", - "id": "d2nlhC", + "id": "QKz1T", "name": "historyList", "width": "fill_container", "layout": "vertical", @@ -7363,7 +7420,7 @@ "children": [ { "type": "frame", - "id": "dgf4z", + "id": "btdwd", "name": "histCard1", "width": "fill_container", "fill": "$bg-card", @@ -7374,7 +7431,7 @@ "children": [ { "type": "frame", - "id": "T432s", + "id": "bcLm2", "name": "histPoster1", "width": 60, "height": 84, @@ -7403,7 +7460,7 @@ "children": [ { "type": "icon", - "id": "W7AOQn", + "id": "t660g6", "name": "histPosterIcon1", "width": 22, "height": 22, @@ -7415,7 +7472,7 @@ }, { "type": "frame", - "id": "Gsi6b", + "id": "xAWyd", "name": "histInfo1", "width": "fill_container", "layout": "vertical", @@ -7423,7 +7480,7 @@ "children": [ { "type": "text", - "id": "Z1fRo2", + "id": "u0AMSK", "name": "histTitle1", "fill": "$text-primary", "textGrowth": "fixed-width", @@ -7435,7 +7492,7 @@ }, { "type": "text", - "id": "eTyd2", + "id": "e4wJgR", "name": "histMeta1", "fill": "$text-muted", "textGrowth": "fixed-width", @@ -7447,14 +7504,14 @@ }, { "type": "frame", - "id": "m7LYz", + "id": "gc5aI", "name": "histUsers1", "gap": 4, "alignItems": "center", "children": [ { "type": "frame", - "id": "gq5MC", + "id": "xDdjd", "name": "histUser1_1", "height": 22, "fill": "#000000", @@ -7468,7 +7525,7 @@ "children": [ { "type": "text", - "id": "oj83K", + "id": "EQeGI", "name": "histUserText1_1", "fill": "#FFFFFF", "content": "nacchofer31", @@ -7482,14 +7539,14 @@ }, { "type": "frame", - "id": "E70aZ", + "id": "wzqLS", "name": "histGenres1", "gap": 4, "alignItems": "center", "children": [ { "type": "frame", - "id": "QVB5k", + "id": "ISDSs", "name": "histGenre1_1", "height": 22, "fill": "$tag-green", @@ -7502,7 +7559,7 @@ "children": [ { "type": "text", - "id": "TmN0t", + "id": "B1Mgv", "name": "histGenreText1_1", "fill": "$accent-green", "content": "Horror", @@ -7518,7 +7575,7 @@ }, { "type": "frame", - "id": "j84n0i", + "id": "B2pWb", "name": "histFav1", "width": 36, "height": 36, @@ -7529,7 +7586,7 @@ "children": [ { "type": "icon", - "id": "lH1qH", + "id": "QtmvJ", "name": "histFavIcon1", "width": 16, "height": 16, @@ -7543,7 +7600,7 @@ }, { "type": "frame", - "id": "kGlKb", + "id": "RrBuq", "name": "histCard2", "width": "fill_container", "fill": "$bg-card", @@ -7554,7 +7611,7 @@ "children": [ { "type": "frame", - "id": "B1BqFF", + "id": "RpuXx", "name": "histPoster2", "width": 60, "height": 84, @@ -7583,7 +7640,7 @@ "children": [ { "type": "icon", - "id": "f5uyRd", + "id": "MRnHO", "name": "histPosterIcon2", "width": 22, "height": 22, @@ -7595,7 +7652,7 @@ }, { "type": "frame", - "id": "cashe", + "id": "irpjG", "name": "histInfo2", "width": "fill_container", "layout": "vertical", @@ -7603,7 +7660,7 @@ "children": [ { "type": "text", - "id": "KmeXG", + "id": "btGpn", "name": "histTitle2", "fill": "$text-primary", "textGrowth": "fixed-width", @@ -7615,7 +7672,7 @@ }, { "type": "text", - "id": "LSxOI", + "id": "e9ibis", "name": "histMeta2", "fill": "$text-muted", "textGrowth": "fixed-width", @@ -7627,14 +7684,14 @@ }, { "type": "frame", - "id": "eN5T8", + "id": "sQNC3", "name": "histUsers2", "gap": 4, "alignItems": "center", "children": [ { "type": "frame", - "id": "OeI5o", + "id": "NaYWN", "name": "histUser2_1", "height": 22, "fill": "$accent-green", @@ -7648,7 +7705,7 @@ "children": [ { "type": "text", - "id": "AVnIa", + "id": "YfynA", "name": "histUserText2_1", "fill": "#14181C", "content": "nacchofer31", @@ -7660,7 +7717,7 @@ }, { "type": "frame", - "id": "uD9te", + "id": "INiau", "name": "histUser2_2", "height": 22, "fill": "$accent-green", @@ -7674,7 +7731,7 @@ "children": [ { "type": "text", - "id": "UZmac", + "id": "AFxv0", "name": "histUserText2_2", "fill": "#14181C", "content": "shoegazer94", @@ -7688,14 +7745,14 @@ }, { "type": "frame", - "id": "iTb25", + "id": "vPTif", "name": "histGenres2", "gap": 4, "alignItems": "center", "children": [ { "type": "frame", - "id": "W16LFB", + "id": "C1VyyD", "name": "histGenre2_1", "height": 22, "fill": "$tag-green", @@ -7708,7 +7765,7 @@ "children": [ { "type": "text", - "id": "dCYf6", + "id": "u4gOut", "name": "histGenreText2_1", "fill": "$accent-green", "content": "Drama", @@ -7720,7 +7777,7 @@ }, { "type": "frame", - "id": "R37tl", + "id": "o9UcE", "name": "histGenre2_2", "height": 22, "fill": "$tag-green", @@ -7733,7 +7790,7 @@ "children": [ { "type": "text", - "id": "FpH7Y", + "id": "jG2yK", "name": "histGenreText2_2", "fill": "$accent-green", "content": "Romance", @@ -7749,7 +7806,7 @@ }, { "type": "frame", - "id": "AJ09U", + "id": "MStSF", "name": "histFav2", "width": 36, "height": 36, @@ -7760,7 +7817,7 @@ "children": [ { "type": "icon", - "id": "GzD2v", + "id": "vXtpw", "name": "histFavIcon2", "width": 16, "height": 16, @@ -7774,7 +7831,7 @@ }, { "type": "frame", - "id": "wHXWI", + "id": "rJOyT", "name": "histCard3", "width": "fill_container", "fill": "$bg-card", @@ -7785,7 +7842,7 @@ "children": [ { "type": "frame", - "id": "LOl8x", + "id": "K1sSTc", "name": "histPoster3", "width": 60, "height": 84, @@ -7814,7 +7871,7 @@ "children": [ { "type": "icon", - "id": "XGqKC", + "id": "J8PsXS", "name": "histPosterIcon3", "width": 22, "height": 22, @@ -7826,7 +7883,7 @@ }, { "type": "frame", - "id": "LQWmw", + "id": "QupHg", "name": "histInfo3", "width": "fill_container", "layout": "vertical", @@ -7834,7 +7891,7 @@ "children": [ { "type": "text", - "id": "iaQg9", + "id": "LOG7U", "name": "histTitle3", "fill": "$text-primary", "textGrowth": "fixed-width", @@ -7846,7 +7903,7 @@ }, { "type": "text", - "id": "Vpe4Z", + "id": "q9nnx", "name": "histMeta3", "fill": "$text-muted", "textGrowth": "fixed-width", @@ -7858,14 +7915,14 @@ }, { "type": "frame", - "id": "Nol4m", + "id": "ltBAB", "name": "histUsers3", "gap": 4, "alignItems": "center", "children": [ { "type": "frame", - "id": "jv76Q", + "id": "TA5Gi", "name": "histUser3_1", "height": 22, "fill": "#000000", @@ -7879,7 +7936,7 @@ "children": [ { "type": "text", - "id": "t8SRI7", + "id": "yQhmh", "name": "histUserText3_1", "fill": "#FFFFFF", "content": "shoegazer94", @@ -7893,14 +7950,14 @@ }, { "type": "frame", - "id": "K6eNGL", + "id": "i2Yqad", "name": "histGenres3", "gap": 4, "alignItems": "center", "children": [ { "type": "frame", - "id": "pJACm", + "id": "VkYqH", "name": "histGenre3_1", "height": 22, "fill": "$bg-elevated", @@ -7913,7 +7970,7 @@ "children": [ { "type": "text", - "id": "fT3tr", + "id": "YEn3o", "name": "histGenreText3_1", "fill": "$text-muted", "content": "Any genre", @@ -7929,7 +7986,7 @@ }, { "type": "frame", - "id": "Mbf3j", + "id": "z86gQ", "name": "histFav3", "width": 36, "height": 36, @@ -7940,7 +7997,7 @@ "children": [ { "type": "icon", - "id": "qic13", + "id": "N6kwn", "name": "histFavIcon3", "width": 16, "height": 16, @@ -7954,7 +8011,7 @@ }, { "type": "frame", - "id": "f0T6F", + "id": "ASdwk", "name": "histCard4", "width": "fill_container", "fill": "$bg-card", @@ -7965,7 +8022,7 @@ "children": [ { "type": "frame", - "id": "RwmLw", + "id": "UtKO2", "name": "histPoster4", "width": 60, "height": 84, @@ -7994,7 +8051,7 @@ "children": [ { "type": "icon", - "id": "rzWAI", + "id": "ZC6j7", "name": "histPosterIcon4", "width": 22, "height": 22, @@ -8006,7 +8063,7 @@ }, { "type": "frame", - "id": "Yhi7F", + "id": "Gzc8S", "name": "histInfo4", "width": "fill_container", "layout": "vertical", @@ -8014,7 +8071,7 @@ "children": [ { "type": "text", - "id": "GlCjX", + "id": "Jwoh3", "name": "histTitle4", "fill": "$text-primary", "textGrowth": "fixed-width", @@ -8026,7 +8083,7 @@ }, { "type": "text", - "id": "s0Bos7", + "id": "F6r6lC", "name": "histMeta4", "fill": "$text-muted", "textGrowth": "fixed-width", @@ -8038,14 +8095,14 @@ }, { "type": "frame", - "id": "wIKNm", + "id": "l1ygi", "name": "histUsers4", "gap": 4, "alignItems": "center", "children": [ { "type": "frame", - "id": "VgWEl", + "id": "GGJ4J", "name": "histUser4_1", "height": 22, "fill": "$accent-orange", @@ -8059,7 +8116,7 @@ "children": [ { "type": "text", - "id": "GSAsJ", + "id": "EOpUA", "name": "histUserText4_1", "fill": "#14181C", "content": "nacchofer31", @@ -8071,7 +8128,7 @@ }, { "type": "frame", - "id": "xh2QT", + "id": "lgc9Y", "name": "histUser4_2", "height": 22, "fill": "$accent-orange", @@ -8085,7 +8142,7 @@ "children": [ { "type": "text", - "id": "M5Rvk", + "id": "KBxuz", "name": "histUserText4_2", "fill": "#14181C", "content": "shoegazer94", @@ -8099,14 +8156,14 @@ }, { "type": "frame", - "id": "we7Xz", + "id": "fKcgC", "name": "histGenres4", "gap": 4, "alignItems": "center", "children": [ { "type": "frame", - "id": "aNs27", + "id": "vBlfN", "name": "histGenre4_1", "height": 22, "fill": "$tag-green", @@ -8119,7 +8176,7 @@ "children": [ { "type": "text", - "id": "msFvf", + "id": "NZy0g", "name": "histGenreText4_1", "fill": "$accent-green", "content": "Comedy", @@ -8135,7 +8192,7 @@ }, { "type": "frame", - "id": "rOU1P", + "id": "H8A3zY", "name": "histFav4", "width": 36, "height": 36, @@ -8146,7 +8203,7 @@ "children": [ { "type": "icon", - "id": "eWtXC", + "id": "DxDP5", "name": "histFavIcon4", "width": 16, "height": 16, @@ -8160,7 +8217,7 @@ }, { "type": "frame", - "id": "wDK7P", + "id": "IjcrA", "name": "histCard5", "width": "fill_container", "fill": "$bg-card", @@ -8171,7 +8228,7 @@ "children": [ { "type": "frame", - "id": "jlTiw", + "id": "SDclH", "name": "histPoster5", "width": 60, "height": 84, @@ -8200,7 +8257,7 @@ "children": [ { "type": "icon", - "id": "C67TiK", + "id": "ftqt4", "name": "histPosterIcon5", "width": 22, "height": 22, @@ -8212,7 +8269,7 @@ }, { "type": "frame", - "id": "LHnqY", + "id": "YKP5i", "name": "histInfo5", "width": "fill_container", "layout": "vertical", @@ -8220,7 +8277,7 @@ "children": [ { "type": "text", - "id": "G3Nc6", + "id": "J3YlP7", "name": "histTitle5", "fill": "$text-primary", "textGrowth": "fixed-width", @@ -8232,7 +8289,7 @@ }, { "type": "text", - "id": "NxqjB", + "id": "LbZV3", "name": "histMeta5", "fill": "$text-muted", "textGrowth": "fixed-width", @@ -8244,14 +8301,14 @@ }, { "type": "frame", - "id": "GK8UN", + "id": "e1MPv2", "name": "histUsers5", "gap": 4, "alignItems": "center", "children": [ { "type": "frame", - "id": "jnXC0", + "id": "GDohb", "name": "histUser5_1", "height": 22, "fill": "#000000", @@ -8265,7 +8322,7 @@ "children": [ { "type": "text", - "id": "ce77Y", + "id": "V33CmZ", "name": "histUserText5_1", "fill": "#FFFFFF", "content": "nacchofer31", @@ -8279,14 +8336,14 @@ }, { "type": "frame", - "id": "oxoAN", + "id": "Se7IU", "name": "histGenres5", "gap": 4, "alignItems": "center", "children": [ { "type": "frame", - "id": "sJWvK", + "id": "k5LdG", "name": "histGenre5_1", "height": 22, "fill": "$tag-green", @@ -8299,7 +8356,7 @@ "children": [ { "type": "text", - "id": "SAWjf", + "id": "Q7TwM", "name": "histGenreText5_1", "fill": "$accent-green", "content": "Sci-Fi", @@ -8315,7 +8372,7 @@ }, { "type": "frame", - "id": "QW2Oh", + "id": "uY81N", "name": "histFav5", "width": 36, "height": 36, @@ -8326,7 +8383,7 @@ "children": [ { "type": "icon", - "id": "UJih3", + "id": "qRpxn", "name": "histFavIcon5", "width": 16, "height": 16, @@ -8342,7 +8399,7 @@ }, { "type": "frame", - "id": "rHxIH", + "id": "k3QINf", "name": "historyFooter", "width": "fill_container", "padding": [ @@ -8356,7 +8413,7 @@ "children": [ { "type": "text", - "id": "OwkiY", + "id": "lx1zD", "name": "historyFooterText", "fill": "$text-muted", "content": "24 movies picked in total", @@ -8369,6 +8426,2278 @@ ] } ] + }, + { + "type": "frame", + "id": "Sw03G", + "x": 3880, + "y": 0, + "name": "Share Card - Cine", + "clip": true, + "width": 1080, + "height": 1080, + "fill": { + "type": "gradient", + "gradientType": "radial", + "enabled": true, + "rotation": 0, + "size": { + "width": 1.4, + "height": 1.4 + }, + "colors": [ + { + "color": "#40BCF460", + "position": 0 + }, + { + "color": "#0C1013", + "position": 0.55 + }, + { + "color": "#14181C", + "position": 1 + } + ] + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "Gtqb3", + "layoutPosition": "absolute", + "x": 0, + "y": 0, + "name": "dicePattern", + "width": 1080, + "height": 1080, + "layout": "none", + "children": [ + { + "type": "icon", + "id": "AYuV8", + "x": 20, + "y": 20, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "pmnh4", + "x": 108, + "y": 20, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "fKGI1", + "x": 196, + "y": 20, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "aOAma", + "x": 284, + "y": 20, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "dMl2j", + "x": 372, + "y": 20, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "cKC44", + "x": 460, + "y": 20, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "t1CDM", + "x": 548, + "y": 20, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "tjdIY", + "x": 636, + "y": 20, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "rzbvv", + "x": 724, + "y": 20, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "BhTZW", + "x": 812, + "y": 20, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "tYNIq", + "x": 900, + "y": 20, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "OOPga", + "x": 988, + "y": 20, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "eTYu5", + "x": 1076, + "y": 20, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "HvsZN", + "x": 64, + "y": 108, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "fyrdA", + "x": 152, + "y": 108, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "MwrlI", + "x": 240, + "y": 108, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "nAEF4", + "x": 328, + "y": 108, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "H81Bz", + "x": 416, + "y": 108, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "vyhf9", + "x": 504, + "y": 108, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "h7uTsx", + "x": 592, + "y": 108, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "U6QkLG", + "x": 680, + "y": 108, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "x3a5N", + "x": 768, + "y": 108, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "i4LR0", + "x": 856, + "y": 108, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "NtCtp", + "x": 944, + "y": 108, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "TLHs9", + "x": 1032, + "y": 108, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "tcMvq", + "x": 1120, + "y": 108, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "T5fEM4", + "x": 20, + "y": 196, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "Q8QIEJ", + "x": 108, + "y": 196, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "a7Yqmd", + "x": 196, + "y": 196, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "q8amjw", + "x": 284, + "y": 196, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "WJ29C", + "x": 372, + "y": 196, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "MRXt3", + "x": 460, + "y": 196, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "pL8me", + "x": 548, + "y": 196, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "iSFZh", + "x": 636, + "y": 196, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "t6dNk", + "x": 724, + "y": 196, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "rGlrh", + "x": 812, + "y": 196, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "E1Mm3", + "x": 900, + "y": 196, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "EuLdY", + "x": 988, + "y": 196, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "jClm1", + "x": 1076, + "y": 196, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "e0iKD", + "x": 64, + "y": 284, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "mswLt", + "x": 152, + "y": 284, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "VtJZx", + "x": 240, + "y": 284, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "yyzRA", + "x": 328, + "y": 284, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "l15q9f", + "x": 416, + "y": 284, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "EWGQW", + "x": 504, + "y": 284, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "BPIak", + "x": 592, + "y": 284, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "hCWFK", + "x": 680, + "y": 284, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "amGKL", + "x": 768, + "y": 284, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "OrxQR", + "x": 856, + "y": 284, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "Uvtlj", + "x": 944, + "y": 284, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "gykZ0", + "x": 1032, + "y": 284, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "s5VEeE", + "x": 1120, + "y": 284, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "RlVB2", + "x": 20, + "y": 372, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "noIwm", + "x": 108, + "y": 372, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "WHXjO", + "x": 196, + "y": 372, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "JKeLW", + "x": 284, + "y": 372, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "SK0YC", + "x": 372, + "y": 372, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "RCyIP", + "x": 460, + "y": 372, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "bvEsO", + "x": 548, + "y": 372, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "cuQt8", + "x": 636, + "y": 372, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "pdySe", + "x": 724, + "y": 372, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "HdEsO", + "x": 812, + "y": 372, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "s6Xx4S", + "x": 900, + "y": 372, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "pgUas", + "x": 988, + "y": 372, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "KCh3n", + "x": 1076, + "y": 372, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "WNsoJ", + "x": 64, + "y": 460, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "V0Bmp", + "x": 152, + "y": 460, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "Bq3Vk", + "x": 240, + "y": 460, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "wPHJP", + "x": 328, + "y": 460, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "WDjW0", + "x": 416, + "y": 460, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "MNoBs", + "x": 504, + "y": 460, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "jSqTH", + "x": 592, + "y": 460, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "eqPIk", + "x": 680, + "y": 460, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "pMuW9", + "x": 768, + "y": 460, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "nahCh", + "x": 856, + "y": 460, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "omZnp", + "x": 944, + "y": 460, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "oPob0", + "x": 1032, + "y": 460, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "v1M3Z", + "x": 1120, + "y": 460, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "n6EO2", + "x": 20, + "y": 548, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "EwsK3", + "x": 108, + "y": 548, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "lfZww", + "x": 196, + "y": 548, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "HakgT", + "x": 284, + "y": 548, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "arTPy", + "x": 372, + "y": 548, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "X2gn1k", + "x": 460, + "y": 548, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "mxo47", + "x": 548, + "y": 548, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "h3WEut", + "x": 636, + "y": 548, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "fkFrZ", + "x": 724, + "y": 548, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "b5qIO", + "x": 812, + "y": 548, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "a17aw3", + "x": 900, + "y": 548, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "Y9ue2C", + "x": 988, + "y": 548, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "kDJRD", + "x": 1076, + "y": 548, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "B1Leia", + "x": 64, + "y": 636, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "iL4FO", + "x": 152, + "y": 636, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "F6KgaR", + "x": 240, + "y": 636, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "Kpcly", + "x": 328, + "y": 636, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "m4GmcZ", + "x": 416, + "y": 636, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "dVzwg", + "x": 504, + "y": 636, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "RzXoQ", + "x": 592, + "y": 636, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "i3v2de", + "x": 680, + "y": 636, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "bOgT3", + "x": 768, + "y": 636, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "c6HVY", + "x": 856, + "y": 636, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "YwzM0", + "x": 944, + "y": 636, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "WuetN", + "x": 1032, + "y": 636, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "ahKdr", + "x": 1120, + "y": 636, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "eobY5", + "x": 20, + "y": 724, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "Sasz5", + "x": 108, + "y": 724, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "QSDo4", + "x": 196, + "y": 724, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "moIYa", + "x": 284, + "y": 724, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "BNMxJ", + "x": 372, + "y": 724, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "xb0DB", + "x": 460, + "y": 724, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "xZvPD", + "x": 548, + "y": 724, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "iFA1a", + "x": 636, + "y": 724, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "JeYAc", + "x": 724, + "y": 724, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "yCuBM", + "x": 812, + "y": 724, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "XsU7X", + "x": 900, + "y": 724, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "N91kxi", + "x": 988, + "y": 724, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "uVYDN", + "x": 1076, + "y": 724, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "I64ba", + "x": 64, + "y": 812, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "zwRl5", + "x": 152, + "y": 812, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "osPGk", + "x": 240, + "y": 812, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "fpZVv", + "x": 328, + "y": 812, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "eAQ2G", + "x": 416, + "y": 812, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "pNZ5B", + "x": 504, + "y": 812, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "Vqiq0", + "x": 592, + "y": 812, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "Z5ftky", + "x": 680, + "y": 812, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "Cre9T", + "x": 768, + "y": 812, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "jrWRU", + "x": 856, + "y": 812, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "sKWcA", + "x": 944, + "y": 812, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "BpFDe", + "x": 1032, + "y": 812, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "OPPOu", + "x": 1120, + "y": 812, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "OJQHU", + "x": 20, + "y": 900, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "j8cLY", + "x": 108, + "y": 900, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "TmtxX", + "x": 196, + "y": 900, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "Q7IsaU", + "x": 284, + "y": 900, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "x3W93", + "x": 372, + "y": 900, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "Q4bR8", + "x": 460, + "y": 900, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "FayLn", + "x": 548, + "y": 900, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "q0GKe", + "x": 636, + "y": 900, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "rcGk4", + "x": 724, + "y": 900, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "E7t6o", + "x": 812, + "y": 900, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "UBkJx", + "x": 900, + "y": 900, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "NAenX", + "x": 988, + "y": 900, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "O4a1vc", + "x": 1076, + "y": 900, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "QE3c1", + "x": 64, + "y": 988, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "j15QD", + "x": 152, + "y": 988, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "kscoz", + "x": 240, + "y": 988, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "VPtYJ", + "x": 328, + "y": 988, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "jisyY", + "x": 416, + "y": 988, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "xRo2G", + "x": 504, + "y": 988, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "XyNRe", + "x": 592, + "y": 988, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "y7cf3J", + "x": 680, + "y": 988, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "c10Gl", + "x": 768, + "y": 988, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "T8P9w6", + "x": 856, + "y": 988, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "XVhpK", + "x": 944, + "y": 988, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "Dwsd0", + "x": 1032, + "y": 988, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "r56o6L", + "x": 1120, + "y": 988, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "DCDU8", + "x": 20, + "y": 1076, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "hHdMn", + "x": 108, + "y": 1076, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "x29Rmj", + "x": 196, + "y": 1076, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "kc10O", + "x": 284, + "y": 1076, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "EccnK", + "x": 372, + "y": 1076, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "j2tD3", + "x": 460, + "y": 1076, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "SDWQj", + "x": 548, + "y": 1076, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "qkcW6", + "x": 636, + "y": 1076, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "k003XO", + "x": 724, + "y": 1076, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "D2e9b", + "x": 812, + "y": 1076, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "I2VpCx", + "x": 900, + "y": 1076, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "xg0F7", + "x": 988, + "y": 1076, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + }, + { + "type": "icon", + "id": "hY8bK", + "x": 1076, + "y": 1076, + "name": "diceCell", + "width": 48, + "height": 48, + "icon": "dices", + "library": "lucide", + "fill": "#40BCF418" + } + ] + }, + { + "type": "frame", + "id": "drRvK", + "name": "shareCardContent", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "padding": [ + 48, + 64, + 64, + 64 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "rdVDX", + "name": "cardTop", + "width": "fill_container", + "layout": "vertical", + "gap": 32, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VPTwo", + "name": "shareTagline", + "fill": "$text-secondary", + "content": "The dice has spoken.", + "textAlign": "center", + "fontFamily": "Plus Jakarta Sans", + "fontSize": 30, + "fontWeight": "700", + "letterSpacing": 1 + }, + { + "type": "frame", + "id": "K0wBw", + "name": "sharePoster", + "width": 360, + "height": 500, + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1505457315458-62417e662cf4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w4NDM0ODN8MHwxfHJhbmRvbXx8fHx8fHx8fHwxNzY5MjA3NjI2fA&ixlib=rb-4.1.0&q=80&w=1080", + "mode": "fill" + }, + "cornerRadius": 24, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#40BCF4A0", + "blur": 120 + } + }, + { + "type": "text", + "id": "Tayy4", + "name": "shareTitle", + "fill": "$text-primary", + "content": "Ghostwatch", + "textAlign": "center", + "fontFamily": "Plus Jakarta Sans", + "fontSize": 64, + "fontWeight": "800" + }, + { + "type": "text", + "id": "ctdwu", + "name": "shareYear", + "fill": "$text-secondary", + "content": "1992", + "textAlign": "center", + "fontFamily": "Inter", + "fontSize": 34, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "TXIBv", + "name": "cardFooter", + "width": "fill_container", + "layout": "vertical", + "gap": 20, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "rLwN0", + "name": "brandRow", + "gap": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "s7AYl", + "name": "shareLogo", + "width": 56, + "height": 56, + "fill": "$accent-green", + "cornerRadius": 14, + "layout": "vertical", + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "Qqtcy", + "name": "shareLogoIcon", + "width": 34, + "height": 34, + "icon": "clapperboard", + "library": "lucide", + "fill": "#14181C" + } + ] + }, + { + "type": "text", + "id": "lWY0T", + "name": "brandText", + "fill": "$text-primary", + "content": "RandomBoxd", + "fontFamily": "Plus Jakarta Sans", + "fontSize": 44, + "fontWeight": "800" + } + ] + }, + { + "type": "text", + "id": "s7UbD", + "name": "brandTagline", + "fill": "$text-muted", + "content": "Never struggle to pick a movie again", + "textAlign": "center", + "fontFamily": "Inter", + "fontSize": 22 + }, + { + "type": "frame", + "id": "iJYRI", + "name": "storeRow", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "S5Yeo", + "name": "playStoreBadge", + "height": 40, + "fill": "$bg-elevated", + "cornerRadius": 20, + "stroke": "$border-subtle", + "strokeWidth": 1, + "gap": 10, + "padding": [ + 0, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "m2m1q", + "name": "playIcon", + "width": 18, + "height": 18, + "icon": "play", + "library": "lucide", + "fill": "$accent-teal" + }, + { + "type": "text", + "id": "NpaPT", + "name": "playStoreText", + "fill": "$text-primary", + "content": "Available on Google Play", + "fontFamily": "Inter", + "fontSize": 20, + "fontWeight": "500" + } + ] + } + ] + } + ] + } + ] + } + ] } ], "variables": {