diff --git a/README.md b/README.md index a797f8a..e6b1462 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![Android-Text-to-Image](https://socialify.git.ci/viethua99/Android-Text-to-Image/image?description=1&descriptionEditable=%F0%9F%9A%80%20Instantly%20generate%20high-quality%20images%20based%20on%20your%20text%20prompt%20%F0%9F%9A%80&font=Inter&logo=https%3A%2F%2Ftabris.com%2Fwp-content%2Fuploads%2F2021%2F06%2Fjetpack-compose-icon_RGB.png&name=1&pattern=Brick%20Wall&theme=Dark)

- + Github - viethua99 @@ -18,7 +18,7 @@ ## Download Go to the [Releases](https://github.com/viethua99/Android-Text-to-Image/releases) to download the latest APK. - + ## Features diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7c787b1..93a89d9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,7 +31,7 @@ android { dependencies { implementation(project(":feature:generate")) - implementation(project(":feature:explore")) + implementation(project(":feature:gallery")) implementation(project(":feature:settings")) implementation(project(":feature:loading")) implementation(project(":feature:result")) diff --git a/app/src/androidTest/java/com/vproject/texttoimage/NavigationTest.kt b/app/src/androidTest/java/com/vproject/texttoimage/NavigationTest.kt index b465a85..446daed 100644 --- a/app/src/androidTest/java/com/vproject/texttoimage/NavigationTest.kt +++ b/app/src/androidTest/java/com/vproject/texttoimage/NavigationTest.kt @@ -19,7 +19,7 @@ import org.junit.Rule import org.junit.Test import kotlin.properties.ReadOnlyProperty import com.vproject.texttoimage.feature.generate.R as generateR -import com.vproject.texttoimage.feature.explore.R as exploreR +import com.vproject.texttoimage.feature.gallery.R as galleryR @HiltAndroidTest class NavigationTest { @@ -46,7 +46,7 @@ class NavigationTest { // The strings used for matching in these tests private val appName by composeTestRule.stringResource(R.string.app_name) private val generate by composeTestRule.stringResource(generateR.string.generate) - private val explore by composeTestRule.stringResource(exploreR.string.explore) + private val gallery by composeTestRule.stringResource(galleryR.string.gallery) @Test fun whenAppStarted_thenFirstScreenIsGenerate() { @@ -60,7 +60,7 @@ class NavigationTest { composeTestRule.apply { onNodeWithContentDescription("settings").assertExists() - onNodeWithText(explore).performClick() + onNodeWithText(gallery).performClick() onNodeWithContentDescription("settings").assertExists() } } @@ -68,23 +68,23 @@ class NavigationTest { @Test(expected = NoActivityResumedException::class) fun givenAppStarted_whenPressingBackButton_thenQuittingApp() { composeTestRule.apply { - onNodeWithText(explore).performClick() + onNodeWithText(gallery).performClick() onNodeWithText(generate).performClick() Espresso.pressBack() } } @Test - fun whenSelectingExploreTab_thenShowExploreNavigationTab() { + fun whenSelectingGalleryTab_thenShowGalleryNavigationTab() { composeTestRule.apply { // Verify that the top bar contains the app name on the first screen. onNodeWithText(appName).assertExists() - // Go to the explore tab, verify that the top bar contains "Explore". This means - // we'll have 2 elements with the text "Explore" on screen. One in the top bar, and + // Go to the gallery tab, verify that the top bar contains "Gallery". This means + // we'll have 2 elements with the text "Gallery" on screen. One in the top bar, and // one in the bottom navigation. - onNodeWithText(explore).performClick() - onAllNodesWithText(explore).assertCountEquals(2) + onNodeWithText(gallery).performClick() + onAllNodesWithText(gallery).assertCountEquals(2) } } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/vproject/texttoimage/TextToImageAppStateTest.kt b/app/src/androidTest/java/com/vproject/texttoimage/TextToImageAppStateTest.kt index 64e000b..de4f529 100644 --- a/app/src/androidTest/java/com/vproject/texttoimage/TextToImageAppStateTest.kt +++ b/app/src/androidTest/java/com/vproject/texttoimage/TextToImageAppStateTest.kt @@ -65,12 +65,12 @@ class TextToImageAppStateTest { currentDestination = SUT.currentDestination?.route - // Navigate to test explore destination + // Navigate to test gallery destination LaunchedEffect(Unit) { - testNavController.setCurrentDestination("test_explore") + testNavController.setCurrentDestination("test_gallery") } } - assertEquals("test_explore", currentDestination) + assertEquals("test_gallery", currentDestination) } @Test @@ -81,7 +81,7 @@ class TextToImageAppStateTest { assertEquals(2, SUT.topLevelDestinations.size) assertTrue(SUT.topLevelDestinations[0].name.contains("generate", true)) - assertTrue(SUT.topLevelDestinations[1].name.contains("explore", true)) + assertTrue(SUT.topLevelDestinations[1].name.contains("gallery", true)) } private fun getCompactWindowClass() = WindowSizeClass.calculateFromSize(DpSize(500.dp, 300.dp)) @@ -95,7 +95,7 @@ private fun rememberTestNavController(): TestNavHostController { navigatorProvider.addNavigator(ComposeNavigator()) graph = createGraph(startDestination = "test_generate") { composable("test_generate") { } - composable("test_explore") { } + composable("test_gallery") { } } } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 448afb0..bc21fe9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,6 @@ - - + navController.navigateToResult(imageUrl, promptContent,"1") + }) settingsScreen( onBackClick = navController::popBackStack, ) loadingScreen( - onImageGenerated = navController::navigateToResult + onImageGenerated = navController::navigateToResult, + onError = { + Toast.makeText(context, it, Toast.LENGTH_LONG).show() + navController.navigateToGenerate() + } + ) + resultScreen( + onBackClick = navController::popBackStack ) - resultScreen() } } \ No newline at end of file diff --git a/app/src/main/java/com/vproject/texttoimage/navigation/TopLevelDestination.kt b/app/src/main/java/com/vproject/texttoimage/navigation/TopLevelDestination.kt index b3b4639..5fb13ae 100644 --- a/app/src/main/java/com/vproject/texttoimage/navigation/TopLevelDestination.kt +++ b/app/src/main/java/com/vproject/texttoimage/navigation/TopLevelDestination.kt @@ -2,6 +2,7 @@ package com.vproject.texttoimage.navigation import androidx.compose.ui.graphics.vector.ImageVector import com.vproject.texttoimage.R +import com.vproject.texttoimage.feature.gallery.R as galleryR import com.vproject.texttoimage.core.designsystem.icon.TextToImageIcons import com.vproject.texttoimage.feature.generate.R as generateR @@ -18,15 +19,15 @@ enum class TopLevelDestination( ) { GENERATE( selectedIcon = TextToImageIcons.RoundedAutoFixNormal, - unselectedIcon = TextToImageIcons.OutlinedAutoFixNormal, + unselectedIcon = TextToImageIcons.RoundedAutoFixNormal, iconTextId = generateR.string.generate, titleTextId = R.string.app_name, ), - EXPLORE( - selectedIcon = TextToImageIcons.RoundedLanguage, - unselectedIcon = TextToImageIcons.OutlinedLanguage, - iconTextId = R.string.explore, - titleTextId = R.string.explore, + GALLERY( + selectedIcon = TextToImageIcons.PhotoLibrary, + unselectedIcon = TextToImageIcons.PhotoLibrary, + iconTextId = galleryR.string.gallery, + titleTextId = R.string.app_name, ) } \ No newline at end of file diff --git a/app/src/main/java/com/vproject/texttoimage/ui/TextToImageApp.kt b/app/src/main/java/com/vproject/texttoimage/ui/TextToImageApp.kt index ebea2b2..ad61e4f 100644 --- a/app/src/main/java/com/vproject/texttoimage/ui/TextToImageApp.kt +++ b/app/src/main/java/com/vproject/texttoimage/ui/TextToImageApp.kt @@ -3,6 +3,8 @@ package com.vproject.texttoimage.ui import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -13,6 +15,7 @@ import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.modifier.modifierLocalConsumer import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.navigation.NavDestination @@ -49,7 +52,7 @@ fun TextToImageApp( } } ) { paddingValues -> - Column(Modifier.fillMaxSize()) { + Column(Modifier.fillMaxSize().padding(paddingValues)) { // Show the top app bar on top level destinations. val topLevelDestination = appState.currentTopLevelDestination topLevelDestination?.let { nonNullTopLevelDestination -> diff --git a/app/src/main/java/com/vproject/texttoimage/ui/TextToImageAppState.kt b/app/src/main/java/com/vproject/texttoimage/ui/TextToImageAppState.kt index e29d43e..0739a39 100644 --- a/app/src/main/java/com/vproject/texttoimage/ui/TextToImageAppState.kt +++ b/app/src/main/java/com/vproject/texttoimage/ui/TextToImageAppState.kt @@ -13,14 +13,14 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import com.vproject.texttoimage.core.ui.TrackDisposableJank -import com.vproject.texttoimage.feature.explore.navigation.exploreRoute -import com.vproject.texttoimage.feature.explore.navigation.navigateToExplore +import com.vproject.texttoimage.feature.gallery.navigation.galleryRoute +import com.vproject.texttoimage.feature.gallery.navigation.navigateToGallery import com.vproject.texttoimage.feature.generate.navigation.generateRoute import com.vproject.texttoimage.feature.generate.navigation.navigateToGenerate import com.vproject.texttoimage.feature.settings.navigation.navigateToSettings import com.vproject.texttoimage.navigation.TopLevelDestination import com.vproject.texttoimage.navigation.TopLevelDestination.GENERATE -import com.vproject.texttoimage.navigation.TopLevelDestination.EXPLORE +import com.vproject.texttoimage.navigation.TopLevelDestination.GALLERY @Composable fun rememberTextToImageAppState( @@ -49,20 +49,20 @@ class TextToImageAppState( val currentTopLevelDestination: TopLevelDestination? @Composable get() = when (currentDestination?.route) { generateRoute -> GENERATE - exploreRoute -> EXPLORE + galleryRoute -> GALLERY else -> null } val shouldShowBottomBar: Boolean @Composable get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact && - (currentTopLevelDestination == GENERATE || currentTopLevelDestination == EXPLORE) + (currentTopLevelDestination == GENERATE || currentTopLevelDestination == GALLERY) /** * Map of top level destinations to be used in the TopBar, BottomBar. The key is the * route. */ - val topLevelDestinations: List = TopLevelDestination.values().asList().filter { it.name == GENERATE.name } + val topLevelDestinations: List = TopLevelDestination.values().asList() /** @@ -88,7 +88,7 @@ class TextToImageAppState( when (topLevelDestination) { GENERATE -> navController.navigateToGenerate(topLevelNavOptions) - EXPLORE -> navController.navigateToExplore(topLevelNavOptions) + GALLERY -> navController.navigateToGallery(topLevelNavOptions) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cb11b58..e42d6f0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,5 +2,5 @@ Text To Image - Explore + Gallery \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/vproject/texttoimage/core/data/repository/image/ImageRepository.kt b/core/data/src/main/kotlin/com/vproject/texttoimage/core/data/repository/image/ImageRepository.kt index 57ed84d..56e0239 100644 --- a/core/data/src/main/kotlin/com/vproject/texttoimage/core/data/repository/image/ImageRepository.kt +++ b/core/data/src/main/kotlin/com/vproject/texttoimage/core/data/repository/image/ImageRepository.kt @@ -1,7 +1,11 @@ package com.vproject.texttoimage.core.data.repository.image +import com.vproject.texttoimage.core.model.data.PromptData import kotlinx.coroutines.flow.Flow interface ImageRepository { - suspend fun generateImage(prompt: String) : Flow + suspend fun generateImage(prompt: String, negativePrompt: String) : Flow + suspend fun fetchQueuedImage(id: Long) : Flow + fun getGeneratedPromptList() : Flow> + fun getTopTrendingPromptList() : Flow> } \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/vproject/texttoimage/core/data/repository/image/OfflineFirstImageRepository.kt b/core/data/src/main/kotlin/com/vproject/texttoimage/core/data/repository/image/OfflineFirstImageRepository.kt index 02acf94..930642e 100644 --- a/core/data/src/main/kotlin/com/vproject/texttoimage/core/data/repository/image/OfflineFirstImageRepository.kt +++ b/core/data/src/main/kotlin/com/vproject/texttoimage/core/data/repository/image/OfflineFirstImageRepository.kt @@ -2,21 +2,110 @@ package com.vproject.texttoimage.core.data.repository.image import com.vproject.texttoimage.core.common.network.TextToImageDispatchers.IO import com.vproject.texttoimage.core.common.network.Dispatcher +import com.vproject.texttoimage.core.database.dao.PromptDao +import com.vproject.texttoimage.core.database.entity.toPromptData +import com.vproject.texttoimage.core.database.entity.toPromptEntity +import com.vproject.texttoimage.core.model.data.PromptData +import com.vproject.texttoimage.core.model.data.PromptStatus +import com.vproject.texttoimage.core.model.data.Style import com.vproject.texttoimage.core.network.TextToImageNetworkDataSource -import com.vproject.texttoimage.core.network.model.TextToImageRequestBody +import com.vproject.texttoimage.core.network.model.ResultWrapper +import com.vproject.texttoimage.core.network.model.safeApiCall +import com.vproject.texttoimage.core.network.model.toPromptData import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import javax.inject.Inject internal class OfflineFirstImageRepository @Inject constructor( private val network: TextToImageNetworkDataSource, + private val promptDao: PromptDao, @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, ) : ImageRepository { + private val topTrendingList = listOf( + PromptData( + status = PromptStatus.Success, + imageUrl = "https://cdn2.stablediffusionapi.com/generations/1eed8268-ba30-4c70-afd6-ab6f2e8ad72f-0.png", + content = "Symmetry Portrait of Storm Trooper, Star Wars, sci-fi, glowing lights!! Intricate, elegant, highly detailed, digital painting, ArtStation, concept art, smooth, sharp focus, illustration, art by Artgerm, Greg Rutkowski, Alphonse Mucha" + ), + PromptData( + status = PromptStatus.Success, + imageUrl = "https://cdn2.stablediffusionapi.com/generations/55b38040-ed64-471f-bf7a-4c3a27add1bc-0.png", + content = "Highly detailed portrait of a sewer Spiderman, tartan hoody by Atey Ghailan, Greg Rutkowski, Greg Tocchini, James Gilleard, Joe Fenton, Kaethe Butcher, gradient red, brown, cream, and white color" + ), + PromptData( + status = PromptStatus.Success, + imageUrl = "https://pub-3626123a908346a7a8be8d9295f44e26.r2.dev/generations/8c253dc9-0bb0-4dd9-adec-f0223fc5299a-0.png", + content = "Beautiful wide shot Tatooine landscape, Luke Skywalker watches binary sunset from moisture farm, Star Wars: A New Hope (1977), Studio Ghibli, Miyazaki, Greg Rutkowski, Alphonse Mucha, Moebius, animation, golden hour, highly detailed, HDR, vivid color, 70mm" + ), + PromptData( + status = PromptStatus.Success, + imageUrl = "https://cdn2.stablediffusionapi.com/generations/18432b6d-957a-4e2a-8718-fbf44fd9bb11-0.png", + content = "Goddess close-up portrait skull with mohawk, ram skull, skeleton, thorax, backbone, jellyfish phoenix head, nautilus, orchid, skull, betta fish, bioluminiscent creatures, intricate artwork by Tooth Wu, WLOP, and Beeple. trending on ArtStation, Greg Rutkowski's very coherent symmetrical artwork" + ), + PromptData( + status = PromptStatus.Success, + imageUrl = "https://cdn2.stablediffusionapi.com/generations/741d882e-356e-43fd-b000-d1cda4a4979c-0.png", + content = "An astronaut holding a beach ball, stranded on an alien island. The ocean is purple, and there are two planets in the sky. Watercolour painting, oil painting, matte painting, cinematic, concept art, HD, colourful, synthwave, Studio Ghibli, purple, astral, nightmare, beautiful, otherworldly" + ), + PromptData( + status = PromptStatus.Success, + imageUrl = "https://cdn2.stablediffusionapi.com/generations/acfada66-57e2-4818-ad79-af2971d04b6d-0.png", + content = "Totem animal totem aztek greeble tribal style fan art ornate fantasy heartstone Ankama GTA5 cover style official Behance HD ArtStation by Jesper Ejsing, RHADS, Makoto Shinkai, Lois van Baarle, Ilya Kuvshinov, Rossdraws totem color pastel vibrant radiating a glowing aura intricate, concept art, matte" + ), + PromptData( + status = PromptStatus.Success, + imageUrl = "https://cdn2.stablediffusionapi.com/generations/aa138ae8-283d-4aca-8f00-14791426a84b-0.png", + content = "Environment castle Nathria in World of Warcraft, gothic style fully developed castle, cinematic, raining, night time, detailed, epic, concept art, matte painting, shafts of lighting, mist, photorealistic, concept art, volumetric light, cinematic epic + rule of thirds, movie concept art, cinematic" + ), + PromptData( + status = PromptStatus.Success, + imageUrl = "https://cdn2.stablediffusionapi.com/generations/85319447-1270-4b77-a929-e7220ab64888-0.png", + content = "Clear portrait of a superhero concept between Superman and Batman, cottagecore, background hyper detailed, character concept, full body, dynamic pose, intricate, highly detailed, digital painting, ArtStation, concept art, sharp focus, illustration, art by Artgerm, Greg Rutkowski, Alphonse Mucha" + ), + PromptData( + status = PromptStatus.Success, + imageUrl = "https://cdn2.stablediffusionapi.com/generations/d0fa0410-2696-42c4-bf3a-48609ff0f2a1-0.png", + content = "A highly detailed matte painting of a man on a hill watching a rocket launch in the distance by Studio Ghibli, Makoto Shinkai, Artgerm, WLOP, Greg Rutkowski, volumetric lighting, Octane Render, 4K resolution, trending on ArtStation, masterpiece" + ), + PromptData( + status = PromptStatus.Success, + imageUrl = "https://cdn2.stablediffusionapi.com/generations/a5aae4b6-b4f2-450c-8551-4ab25ea2f292-0.png", + content = "Highly detailed infographic of retrofuturism cars found in Popular Mechanics magazine | vintage | intricate detail | digital art | digital painting | concept art | poster | award winning | max detail" + ) + ) - override suspend fun generateImage(prompt: String): Flow = withContext(ioDispatcher) { - val output = network.postTextToImage(TextToImageRequestBody(prompt = prompt, negative_prompt = "")).output.first() - flow { emit(output) } + + override suspend fun generateImage(prompt: String, negativePrompt: String): Flow = flow { + val resultWrapper = safeApiCall(ioDispatcher) { network.postTextToImage(prompt, negativePrompt) } + if (resultWrapper is ResultWrapper.Success) { + val promptData = resultWrapper.data.toPromptData().copy(content = prompt) + promptDao.insertOrIgnorePrompt(promptData.toPromptEntity()) + emit(promptData) + } else { + emit(PromptData(status = PromptStatus.Error, content = prompt)) + } + } + + override suspend fun fetchQueuedImage(id: Long): Flow = flow { + val resultWrapper = safeApiCall(ioDispatcher) { network.fetchQueuedImage(id) } + if (resultWrapper is ResultWrapper.Success) { + val promptData = resultWrapper.data.toPromptData() + promptDao.insertOrIgnorePrompt(promptData.toPromptEntity()) + emit(promptData) + } else { + emit(PromptData(status = PromptStatus.Error)) + } } + + override fun getGeneratedPromptList(): Flow> { + return promptDao.getGeneratedPromptEntities().map { promptEntities -> + promptEntities.map { it.toPromptData() } + } + } + + override fun getTopTrendingPromptList(): Flow> = + flow { emit(topTrendingList) } } \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/vproject/texttoimage/core/data/repository/style/OfflineFirstStyleRepository.kt b/core/data/src/main/kotlin/com/vproject/texttoimage/core/data/repository/style/OfflineFirstStyleRepository.kt index 4720255..6d68436 100644 --- a/core/data/src/main/kotlin/com/vproject/texttoimage/core/data/repository/style/OfflineFirstStyleRepository.kt +++ b/core/data/src/main/kotlin/com/vproject/texttoimage/core/data/repository/style/OfflineFirstStyleRepository.kt @@ -9,81 +9,73 @@ internal class OfflineFirstStyleRepository @Inject constructor() : StyleReposito private val styleList = mutableListOf( Style( "1", - "NSFW", - "https://cdn.stablediffusionapi.com/generations/de5977b7-3410-4199-8d96-00c82bd78f1b-0.png", - "nudity content, nsfw, boobs, sex, body, thick" + "No Style", + "https://us.123rf.com/450wm/oksanaoo/oksanaoo1710/oksanaoo171000062/88555541-vector-icon-prohibiting-sign-impossible-stop-and-ban-sign-vector-black-icon-on-white-background.jpg?ver=6", + "", + "" + ), + Style( + "2", + "Anime", + "https://cdn2.stablediffusionapi.com/generations/535d2051-e532-46ca-ad28-202c3431b14f-0.png", + "anime artwork, anime style, key visual, vibrant, studio anime, highly detailed", + "photo, deformed, black and white, realism, disfigured, low contrast" ), Style( "3", - "Building", - "https://cdn.stablediffusionapi.com/generations/dea02ced-e94a-4f7e-b7cf-cfa0ed394796-0.png", - "shot 35 mm, realism, octane render, 8k, trending on artstation, 35 mm camera, unreal engine, hyper detailed, photo - realistic maximum detail, volumetric light, realistic matte painting, hyper photorealistic, trending on artstation, ultra - detailed, realistic" + "Photography", + "https://cdn2.stablediffusionapi.com/generations/10a983ac-ea22-4e58-bce7-388fa211cf62-0.png", + "cinematic photo . 35mm photograph, film, bokeh, professional, 4k, highly detailed", + "drawing, painting, crayon, sketch, graphite, impressionist, noisy, blurry, soft, deformed, ugly" ), Style( "4", - "Cartoon Character", - "https://cdn.stablediffusionapi.com/generations/87e8bcf4-435d-4415-8a80-0b68e85da425-0.png", - "anthro, very cute kid's film character, disney pixar zootopia character concept artwork, 3d concept, detailed fur, high detail iconic character for upcoming film, trending on artstation, character design, 3d artistic render, highly detailed, octane, blender, cartoon, shadows, lighting" + "NSFW", + "https://cdn.stablediffusionapi.com/generations/de5977b7-3410-4199-8d96-00c82bd78f1b-0.png", + "nudity content, nsfw, boobs, sex, body, thick", + "" ), Style( "5", - "Concept Art", - "https://cdn.stablediffusionapi.com/generations/e120b52f-f8ef-4b00-8545-fe461903e935-0.png", - "character sheet, concept design, contrast, style by kim jung gi, zabrocki, karlkka, jayison devadas, trending on artstation, 8k, ultra wide angle, pincushion lens effect" + "Fantasy Art", + "https://cdn2.stablediffusionapi.com/generations/2464ede4-4538-438e-b364-9fcc858a43d4-0.png", + "ethereal fantasy concept art of. magnificent, celestial, ethereal, painterly, epic, majestic, magical, fantasy art, cover art, dreamy", + "photographic, realistic, realism, 35mm film, dslr, cropped, frame, text, deformed, glitch, noise, noisy, off-center, deformed, cross-eyed, closed eyes, bad anatomy, ugly, disfigured, sloppy, duplicate, mutated, black and white" ), Style( "6", - "Cyberpunk", - "https://cdn.stablediffusionapi.com/generations/f3e8ec1a-fa04-49f9-be00-d8cf477a3e99-0.png", - "cyberpunk, in heavy raining futuristic tokyo rooftop cyberpunk night, sci-fi, fantasy, intricate, very very beautiful, elegant, neon light, highly detailed, digital painting, artstation, concept art, soft light, hdri, smooth, sharp focus, illustration, art by tian zi and craig mullins and wlop and alphonse much" + "Concept Art", + "https://cdn2.stablediffusionapi.com/generations/b1478590-9382-42f6-b7e4-484669020d3d-0.png", + "concept art. digital artwork, illustrative, painterly, matte painting, highly detailed", + "photo, photorealistic, realism, ugly" ), Style( "7", - "Digital Art", - "https://cdn.stablediffusionapi.com/generations/39ced130-9d78-46be-8fa7-c29c78a16fed-0.png", - "ultra realistic, concept art, intricate details, highly detailed, photorealistic, octane render, 8k, unreal engine, sharp focus, volumetric lighting unreal engine. art by artgerm and alphonse mucha" + "Isometric", + "https://cdn2.stablediffusionapi.com/generations/a93655ef-a71f-4777-8471-195b78bf2330-0.png", + "isometric style . vibrant, beautiful, crisp, detailed, ultra detailed, intricate", + "deformed, mutated, ugly, disfigured, blur, blurry, noise, noisy, realistic, photographic" ), Style( "8", - "Drawing", - "https://cdn.stablediffusionapi.com/generations/29ce2157-a1eb-4356-81c6-e9a57002d5a4-0.png", - "cute, funny, centered, award winning watercolor pen illustration, detailed, disney, isometric illustration, drawing, by Stephen Hillenburg, Matt Groening, Albert Uderzo" + "Cyberpunk", + "https://cdn2.stablediffusionapi.com/generations/06324b56-9d94-40af-9ee0-04abbd86e4ba-0.png", + "vaporwave synthwave style . cyberpunk, neon, vibes, stunningly beautiful, crisp, detailed, sleek, ultramodern, high contrast, cinematic composition", + "illustration, painting, crayon, graphite, abstract, glitch, deformed, mutated, ugly, disfigured" ), Style( "9", - "Fashion", - "https://cdn.stablediffusionapi.com/generations/048349f5-48e3-47b1-ac24-72f88a5f6277-0.png", - "photograph of a Fashion model, full body, highly detailed and intricate, golden ratio, vibrant colors, hyper maximalist, futuristic, city background, luxury, elite, cinematic, fashion, depth of field, colorful, glow, trending on artstation, ultra high detail, ultra realistic, cinematic lighting, focused, 8k," + "Claymation", + "https://cdn2.stablediffusionapi.com/generations/29446c1a-99c1-4460-8e0d-beb101daf20e-0.png", + "claymation style. sculpture, clay art, centered composition, play-doh", + "sloppy, messy, grainy, highly detailed, ultra textured, photo, mutated" ), Style( "10", - "Landscape", - "https://cdn.stablediffusionapi.com/generations/58c227f3-7144-4a25-972b-087618f2c93c-0.png", - "birds in the sky, waterfall close shot 35 mm, realism, octane render, 8 k, exploration, cinematic, trending on artstation, 35 mm camera, unreal engine, hyper detailed, photo - realistic maximum detail, volumetric light, moody cinematic epic concept art, realistic matte painting, hyper photorealistic, epic, trending on artstation, movie concept art, cinematic composition, ultra - detailed, realistic" - ), - Style( - "11", - "Portrait", - "https://cdn.stablediffusionapi.com/generations/6e74a0b0-77d4-4f0e-b6ba-2754c741f955-0.png", - "portrait photo, photograph, highly detailed face, depth of field, moody light, golden hour, style by Dan Winters, Russell James, Steve McCurry, centered, extremely detailed, Nikon D850, award winning photography" - ), - Style( - "12", - "Space", - "https://cdn.stablediffusionapi.com/generations/ee8b516d-74db-4e19-af8a-0c8d54a399a5-0.png", - "by Andrew McCarthy, Navaneeth Unnikrishnan, Manuel Dietrich, photo realistic, 8 k, cinematic lighting, hd, atmospheric, hyperdetailed, trending on artstation, deviantart, photography, glow effect" - ), - Style( - "13", - "Steampunk", - "https://cdn.stablediffusionapi.com/generations/781ad8c1-85bf-4ed7-99fa-bcbb6eb318ae-0.png", - "steampunk cybernetic biomechanical, 3d model, very coherent symmetrical artwork, unreal engine realistic render, 8k, micro detail, intricate, elegant, highly detailed, centered, digital painting, artstation, smooth, sharp focus, illustration, artgerm, Caio Fantini, wlop" - ), - Style( - "14", - "Vehicles", - "https://pub-8b49af329fae499aa563997f5d4068a4.r2.dev/generations/95c0aac6-cfd5-467a-a3a3-d65808462f49-0.png", - " photorealistic, vivid, sharp focus, reflection, refraction, sunrays, very detailed, intricate, intense cinematic composition" + "Low Poly", + "https://cdn2.stablediffusionapi.com/generations/0592398c-2299-45bb-a5ad-dbce62fa8547-0.png", + "clow-poly style. ambient occlusion, low-poly game art, polygon mesh, jagged, blocky, wireframe edges, centered composition", + "noisy, sloppy, messy, grainy, highly detailed, ultra textured, photo" ), ) diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index 446cb8b..76047cc 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -12,6 +12,8 @@ android { } dependencies { + implementation(project(":core:model")) + implementation(libs.kotlinx.coroutines.android) androidTestImplementation(project(":core:testing")) diff --git a/core/database/src/androidTest/java/com/vproject/texttoimage/core/database/TextToImageDaoTest.kt b/core/database/src/androidTest/java/com/vproject/texttoimage/core/database/PromptDaoTest.kt similarity index 72% rename from core/database/src/androidTest/java/com/vproject/texttoimage/core/database/TextToImageDaoTest.kt rename to core/database/src/androidTest/java/com/vproject/texttoimage/core/database/PromptDaoTest.kt index 069c5a6..1d8723b 100644 --- a/core/database/src/androidTest/java/com/vproject/texttoimage/core/database/TextToImageDaoTest.kt +++ b/core/database/src/androidTest/java/com/vproject/texttoimage/core/database/PromptDaoTest.kt @@ -3,8 +3,8 @@ package com.vproject.texttoimage.core.database import android.content.Context import androidx.room.Room import androidx.test.core.app.ApplicationProvider -import com.vproject.texttoimage.core.database.dao.TextToImageDao -import com.vproject.texttoimage.core.database.entity.TextToImageEntity +import com.vproject.texttoimage.core.database.dao.PromptDao +import com.vproject.texttoimage.core.database.entity.PromptEntity import dagger.hilt.android.testing.HiltAndroidTest import kotlinx.coroutines.test.runTest import org.junit.Before @@ -12,26 +12,26 @@ import org.junit.Test import kotlin.test.assertEquals @HiltAndroidTest -class TextToImageDaoTest { - private lateinit var SUT: TextToImageDao +class PromptDaoTest { + private lateinit var SUT: PromptDao private lateinit var textToImageDatabase: TextToImageDatabase @Before fun setUp() { val context = ApplicationProvider.getApplicationContext() textToImageDatabase = Room.inMemoryDatabaseBuilder(context, TextToImageDatabase::class.java).build() - SUT = textToImageDatabase.textToImageDao() + SUT = textToImageDatabase.promptDao() } @Test fun givenTextToImageDao_whenFetchingTextToImage_thenImageRetrievedSuccess() = runTest { - val textToImageEntity = TextToImageEntity( + val promptEntity = PromptEntity( id = 100, status = "", generateTime = 0.0 ) - SUT.insertOrIgnoreTextToImage(textToImageEntity) + SUT.insertOrIgnoreTextToImage(promptEntity) val savedTextToImageEntity = SUT.getTextToImageEntity(100) diff --git a/core/database/src/main/java/com/vproject/texttoimage/core/database/TextToImageDatabase.kt b/core/database/src/main/java/com/vproject/texttoimage/core/database/TextToImageDatabase.kt index d89d1bb..199172a 100644 --- a/core/database/src/main/java/com/vproject/texttoimage/core/database/TextToImageDatabase.kt +++ b/core/database/src/main/java/com/vproject/texttoimage/core/database/TextToImageDatabase.kt @@ -2,10 +2,10 @@ package com.vproject.texttoimage.core.database import androidx.room.Database import androidx.room.RoomDatabase -import com.vproject.texttoimage.core.database.dao.TextToImageDao -import com.vproject.texttoimage.core.database.entity.TextToImageEntity +import com.vproject.texttoimage.core.database.dao.PromptDao +import com.vproject.texttoimage.core.database.entity.PromptEntity -@Database(entities = [TextToImageEntity::class], version = 1) +@Database(entities = [PromptEntity::class], version = 1) abstract class TextToImageDatabase : RoomDatabase() { - abstract fun textToImageDao(): TextToImageDao + abstract fun promptDao(): PromptDao } \ No newline at end of file diff --git a/core/database/src/main/java/com/vproject/texttoimage/core/database/dao/PromptDao.kt b/core/database/src/main/java/com/vproject/texttoimage/core/database/dao/PromptDao.kt new file mode 100644 index 0000000..462cdd4 --- /dev/null +++ b/core/database/src/main/java/com/vproject/texttoimage/core/database/dao/PromptDao.kt @@ -0,0 +1,26 @@ +package com.vproject.texttoimage.core.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.vproject.texttoimage.core.database.entity.PromptEntity +import kotlinx.coroutines.flow.Flow + +/** + * DAO for [PromptDao] access + */ +@Dao +interface PromptDao { + @Query( + value = """ + SELECT * FROM prompt + WHERE status = "success" + ORDER BY id DESC + """, + ) + fun getGeneratedPromptEntities(): Flow> + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insertOrIgnorePrompt(promptEntity: PromptEntity) +} \ No newline at end of file diff --git a/core/database/src/main/java/com/vproject/texttoimage/core/database/dao/TextToImageDao.kt b/core/database/src/main/java/com/vproject/texttoimage/core/database/dao/TextToImageDao.kt deleted file mode 100644 index 57ccdf7..0000000 --- a/core/database/src/main/java/com/vproject/texttoimage/core/database/dao/TextToImageDao.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.vproject.texttoimage.core.database.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.vproject.texttoimage.core.database.entity.TextToImageEntity - -/** - * DAO for [TextToImageEntity] access - */ -@Dao -interface TextToImageDao { - @Query( - value = """ - SELECT * FROM textToImage - WHERE id = :id - """ - ) - suspend fun getTextToImageEntity(id: Long): TextToImageEntity - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertOrIgnoreTextToImage(textToImageEntity: TextToImageEntity) -} \ No newline at end of file diff --git a/core/database/src/main/java/com/vproject/texttoimage/core/database/di/DaoModule.kt b/core/database/src/main/java/com/vproject/texttoimage/core/database/di/DaoModule.kt index 7c5e899..72d40b6 100644 --- a/core/database/src/main/java/com/vproject/texttoimage/core/database/di/DaoModule.kt +++ b/core/database/src/main/java/com/vproject/texttoimage/core/database/di/DaoModule.kt @@ -1,7 +1,7 @@ package com.vproject.texttoimage.core.database.di import com.vproject.texttoimage.core.database.TextToImageDatabase -import com.vproject.texttoimage.core.database.dao.TextToImageDao +import com.vproject.texttoimage.core.database.dao.PromptDao import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -11,7 +11,7 @@ import dagger.hilt.components.SingletonComponent @InstallIn(SingletonComponent::class) object DaoModule { @Provides - fun providesTextToImageDao( + fun providesPromptDao( database: TextToImageDatabase, - ): TextToImageDao = database.textToImageDao() + ): PromptDao = database.promptDao() } \ No newline at end of file diff --git a/core/database/src/main/java/com/vproject/texttoimage/core/database/entity/PromptEntity.kt b/core/database/src/main/java/com/vproject/texttoimage/core/database/entity/PromptEntity.kt new file mode 100644 index 0000000..e75f0a8 --- /dev/null +++ b/core/database/src/main/java/com/vproject/texttoimage/core/database/entity/PromptEntity.kt @@ -0,0 +1,29 @@ +package com.vproject.texttoimage.core.database.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.vproject.texttoimage.core.model.data.PromptData +import com.vproject.texttoimage.core.model.data.PromptStatus + +@Entity(tableName = "prompt") +data class PromptEntity( + @PrimaryKey + val id: Long, + + @ColumnInfo + val status: String, + + @ColumnInfo + val imageUrl: String, + + @ColumnInfo + val content: String, +) + +fun PromptEntity.toPromptData(): PromptData = + PromptData(id, PromptStatus.values().find { it.value == status } + ?: throw IllegalArgumentException("Illegal Argument Exception"), imageUrl, content) + +fun PromptData.toPromptEntity(): PromptEntity = + PromptEntity(id, status.value, imageUrl ?: "", content) \ No newline at end of file diff --git a/core/database/src/main/java/com/vproject/texttoimage/core/database/entity/TextToImageEntity.kt b/core/database/src/main/java/com/vproject/texttoimage/core/database/entity/TextToImageEntity.kt deleted file mode 100644 index 9061b6f..0000000 --- a/core/database/src/main/java/com/vproject/texttoimage/core/database/entity/TextToImageEntity.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.vproject.texttoimage.core.database.entity - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = "textToImage") -data class TextToImageEntity( - @PrimaryKey - val id: Long, - - @ColumnInfo - val status: String, - - @ColumnInfo - val generateTime: Double, -) \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/Button.kt b/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/Button.kt index 8965f64..d1501a3 100644 --- a/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/Button.kt +++ b/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/Button.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.FilledIconToggleButton @@ -46,10 +47,11 @@ fun TextToImageFilledButton( onClick = onClick, modifier = modifier, enabled = enabled, + shape = RoundedCornerShape(8.dp), contentPadding = contentPadding, content = content, colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.onBackground + containerColor = MaterialTheme.colorScheme.primary ) ) } diff --git a/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/DynamicAsyncImage.kt b/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/DynamicAsyncImage.kt index f3dd882..f72831b 100644 --- a/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/DynamicAsyncImage.kt +++ b/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/DynamicAsyncImage.kt @@ -1,10 +1,23 @@ package com.vproject.texttoimage.core.designsystem.component +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.vproject.texttoimage.core.designsystem.R import com.vproject.texttoimage.core.designsystem.theme.LocalTintTheme /** @@ -14,14 +27,19 @@ import com.vproject.texttoimage.core.designsystem.theme.LocalTintTheme fun DynamicAsyncImage( imageUrl: String, contentDescription: String?, - modifier: Modifier = Modifier, - placeholder: Painter? = null, + modifier: Modifier = Modifier ) { val iconTint = LocalTintTheme.current.iconTint - AsyncImage( - placeholder = placeholder, + SubcomposeAsyncImage( + loading = { + Box(modifier = Modifier.size(5.dp)) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.primary) + } + }, model = imageUrl, contentDescription = contentDescription, + contentScale = ContentScale.Crop, colorFilter = if (iconTint != null) ColorFilter.tint(iconTint) else null, modifier = modifier, ) diff --git a/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/Navigation.kt b/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/Navigation.kt index c026024..a1bae77 100644 --- a/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/Navigation.kt +++ b/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/Navigation.kt @@ -92,10 +92,10 @@ fun RowScope.TextToImageNavigationBarItem( */ object TextToImageNavigationDefaults { @Composable - fun navigationContentColor() = MaterialTheme.colorScheme.onSurfaceVariant + fun navigationContentColor() = MaterialTheme.colorScheme.onSurface @Composable - fun navigationSelectedItemColor() = MaterialTheme.colorScheme.onSurface + fun navigationSelectedItemColor() = MaterialTheme.colorScheme.primary @Composable fun navigationIndicatorColor() = MaterialTheme.colorScheme.surface diff --git a/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/Slider.kt b/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/Slider.kt index 6c3ed75..5e437f4 100644 --- a/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/Slider.kt +++ b/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/Slider.kt @@ -34,9 +34,9 @@ fun TextToImageSlider( Slider( value = value, colors = SliderDefaults.colors( - thumbColor = MaterialTheme.colorScheme.onSurface, - activeTrackColor = MaterialTheme.colorScheme.onSurface, - inactiveTrackColor = MaterialTheme.colorScheme.background, + thumbColor = MaterialTheme.colorScheme.primary, + activeTrackColor = MaterialTheme.colorScheme.primary, + inactiveTrackColor = MaterialTheme.colorScheme.onSecondary, ), onValueChange = onValueChange, valueRange = valueRange, diff --git a/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/TextField.kt b/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/TextField.kt index 870a91a..e17a6c5 100644 --- a/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/TextField.kt +++ b/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/component/TextField.kt @@ -73,7 +73,7 @@ fun TextToImageTextField( decorationBox = { innerTextField -> Box( modifier = modifier - .border(2.dp, MaterialTheme.colorScheme.onSurface, RoundedCornerShape(10)) + .border(2.dp, MaterialTheme.colorScheme.primary, RoundedCornerShape(10)) .height(TextToImageTextFieldDefaults.Height) .background(MaterialTheme.colorScheme.surface) .padding( @@ -90,7 +90,7 @@ fun TextToImageTextField( fontSize = TextToImageTextFieldDefaults.MainHintFontSize ) val subHintStyle = SpanStyle( - color = MaterialTheme.colorScheme.onSecondary, + color = MaterialTheme.colorScheme.onSurface, fontSize = TextToImageTextFieldDefaults.SubHintFontSize ) TextToImageTextFieldHint( @@ -179,7 +179,7 @@ private fun TextToImageTextFieldCornerIcons( * Text To Image text field default values. */ object TextToImageTextFieldDefaults { - val Height = 160.dp + val Height = 200.dp val InnerTopPadding = 16.dp val InnerHorizontalPadding = 16.dp val InnerBottomPadding = 10.dp diff --git a/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/icon/TextToImageIcons.kt b/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/icon/TextToImageIcons.kt index 749f411..3dabe57 100644 --- a/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/icon/TextToImageIcons.kt +++ b/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/icon/TextToImageIcons.kt @@ -1,10 +1,13 @@ package com.vproject.texttoimage.core.designsystem.icon import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BrowseGallery import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.History +import androidx.compose.material.icons.filled.PhotoLibrary import androidx.compose.material.icons.outlined.AutoFixNormal +import androidx.compose.material.icons.outlined.BrowseGallery import androidx.compose.material.icons.outlined.DarkMode import androidx.compose.material.icons.outlined.Favorite import androidx.compose.material.icons.outlined.FavoriteBorder @@ -14,6 +17,7 @@ import androidx.compose.material.icons.outlined.Person import androidx.compose.material.icons.outlined.ReceiptLong import androidx.compose.material.icons.outlined.Translate import androidx.compose.material.icons.outlined.VpnKey +import androidx.compose.material.icons.rounded.AllInclusive import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material.icons.rounded.AutoFixNormal import androidx.compose.material.icons.rounded.Language @@ -26,9 +30,7 @@ import androidx.compose.ui.graphics.vector.ImageVector object TextToImageIcons { val RoundedArrowBack = Icons.Rounded.ArrowBack val RoundedSettings = Icons.Rounded.Settings - val RoundedAutoFixNormal = Icons.Rounded.AutoFixNormal - val OutlinedAutoFixNormal = Icons.Outlined.AutoFixNormal - val RoundedLanguage = Icons.Rounded.Language + val RoundedAutoFixNormal = Icons.Rounded.AllInclusive val OutlinedLanguage = Icons.Outlined.Language val DefaultHistory = Icons.Default.History val DefaultClose = Icons.Default.Close @@ -38,4 +40,5 @@ object TextToImageIcons { val OutlinedInfo = Icons.Outlined.Info val OutlinedReceiptLong = Icons.Outlined.ReceiptLong val OutlinedVpnKey = Icons.Outlined.VpnKey + val PhotoLibrary = Icons.Filled.PhotoLibrary } \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/theme/Color.kt b/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/theme/Color.kt index a530233..1f6b849 100644 --- a/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/theme/Color.kt +++ b/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/theme/Color.kt @@ -6,37 +6,12 @@ import androidx.compose.ui.graphics.Color * Color name convention of Text To Image is based on Color's number of Material Design. * Reference: https://m2.material.io/design/color/the-color-system.html#tools-for-picking-colors */ -internal val Blue10 = Color(0xFF001F28) -internal val Blue20 = Color(0xFF003544) -internal val Blue30 = Color(0xFF004D61) -internal val Blue40 = Color(0xFF006780) -internal val Blue80 = Color(0xFF5DD5FC) -internal val Blue90 = Color(0xFFB8EAFF) -internal val DarkPurpleGray10 = Color(0xFF201A1B) -internal val DarkPurpleGray20 = Color(0xFF362F30) -internal val DarkPurpleGray90 = Color(0xFFECDFE0) -internal val DarkPurpleGray95 = Color(0xFFFAEEEF) -internal val DarkPurpleGray99 = Color(0xFFFCFCFC) -internal val Orange10 = Color(0xFF380D00) -internal val Orange20 = Color(0xFF5B1A00) -internal val Orange30 = Color(0xFF812800) -internal val Orange40 = Color(0xFFA23F16) -internal val Orange80 = Color(0xFFFFB59B) -internal val Orange90 = Color(0xFFFFDBCF) -internal val Purple10 = Color(0xFF36003C) -internal val Purple20 = Color(0xFF560A5D) -internal val Purple30 = Color(0xFF702776) -internal val Purple40 = Color(0xFF8B418F) -internal val Purple80 = Color(0xFFCCFF90) -internal val Purple90 = Color(0xFFFFD6FA) -internal val PurpleGray30 = Color(0xFF4D444C) -internal val PurpleGray50 = Color(0xFF7F747C) -internal val PurpleGray60 = Color(0xFF998D96) -internal val PurpleGray80 = Color(0xFFD0C3CC) -internal val PurpleGray90 = Color(0xFFEDDEE8) -internal val Red10 = Color(0xFF410002) -internal val Red20 = Color(0xFF690005) -internal val Red30 = Color(0xFF93000A) -internal val Red40 = Color(0xFFBA1A1A) -internal val Red80 = Color(0xFFFFB4AB) -internal val Red90 = Color(0xFFFFDAD6) +internal val Grey300 = Color(0xFF9CA3AF) +internal val Blue600 = Color(0xFF2C83FF) +internal val Blue800 = Color(0xFF005FE6) +internal val Black900 = Color(0xFF111827) +internal val White50 = Color(0xFFFFFFFF) +internal val White300 = Color(0xFFE3E3E3) + + + diff --git a/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/theme/Theme.kt b/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/theme/Theme.kt index f18fa5f..9b42863 100644 --- a/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/theme/Theme.kt +++ b/core/designsystem/src/main/java/com/vproject/texttoimage/core/designsystem/theme/Theme.kt @@ -15,16 +15,12 @@ import androidx.compose.ui.unit.dp */ @VisibleForTesting val LightColorScheme = lightColorScheme( - primary = Color(0xFFFFFFFF), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Color(0xffec4079), - onPrimaryContainer = Color(0XFFf06291), - secondary = Color(0xFFFFFFFF), - onSecondary = Color(0xFFd81b5f), - background = Color(0xFF9E9E9E), - onBackground = Color(0xFFAD1457), - surface = Color(0xFFFFFFFF), - onSurface = Color(0xFFAD1457), + primary = Blue800, + onPrimary = White50, + secondary = White50, + onSecondary = Grey300, + surface = White50, + onSurface = Black900, ) /** @@ -32,16 +28,12 @@ val LightColorScheme = lightColorScheme( */ @VisibleForTesting val DarkColorScheme = darkColorScheme( - primary = Color(0xFF0D47A1), - onPrimary = Color(0xFFFFFFFF), - primaryContainer = Purple90, - onPrimaryContainer = Purple10, - secondary = Color(0xFFFFFFFF), - onSecondary = Color(0xFFBDBDBD), - background = DarkPurpleGray99, - onBackground = DarkPurpleGray10, - surface = Color(0xFF212121), - onSurface = Color(0xFFBDBDBD), + primary = Blue600, + onPrimary = White300, + secondary = White50, + onSecondary = Grey300, + surface = Black900, + onSurface = White300, ) /** diff --git a/core/domain/src/main/java/com/vproject/texttoimage/core/domain/GenerateImageUseCase.kt b/core/domain/src/main/java/com/vproject/texttoimage/core/domain/GenerateImageUseCase.kt index 81072df..3a394cb 100644 --- a/core/domain/src/main/java/com/vproject/texttoimage/core/domain/GenerateImageUseCase.kt +++ b/core/domain/src/main/java/com/vproject/texttoimage/core/domain/GenerateImageUseCase.kt @@ -2,8 +2,12 @@ package com.vproject.texttoimage.core.domain import com.vproject.texttoimage.core.data.repository.image.ImageRepository import com.vproject.texttoimage.core.data.repository.style.StyleRepository +import com.vproject.texttoimage.core.model.data.PromptData +import com.vproject.texttoimage.core.model.data.PromptStatus +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import javax.inject.Inject /** @@ -13,9 +17,20 @@ class GenerateImageUseCase @Inject constructor( private val styleRepository: StyleRepository, private val imageRepository: ImageRepository, ) { - operator fun invoke(prompt: String, styleId: String): Flow { + operator fun invoke(prompt: String, styleId: String): Flow { return styleRepository.getStyle(styleId).flatMapLatest { style -> - imageRepository.generateImage("$prompt, ${style.fullDescription}") + imageRepository.generateImage("$prompt, ${style.prompt}", style.negativePrompt).flatMapLatest { promptData -> + when (promptData.status) { + PromptStatus.Processing -> { + delay(40000) + imageRepository.fetchQueuedImage(promptData.id) + } + + else -> { + flow { emit(promptData) } + } + } + } } } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/vproject/texttoimage/core/domain/GetFavorableStyleListUseCase.kt b/core/domain/src/main/java/com/vproject/texttoimage/core/domain/GetFavorableStyleListUseCase.kt index 7f3ad41..102086b 100644 --- a/core/domain/src/main/java/com/vproject/texttoimage/core/domain/GetFavorableStyleListUseCase.kt +++ b/core/domain/src/main/java/com/vproject/texttoimage/core/domain/GetFavorableStyleListUseCase.kt @@ -21,7 +21,7 @@ class GetFavorableStyleListUseCase @Inject constructor( ) { userData, styles -> styles.map { style -> FavorableStyle(style = style, isFavorite = style.id in userData.favoriteStyleIds) - } + }.sortedBy { !it.isFavorite } } } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/vproject/texttoimage/core/domain/GetGeneratedPromptListUseCase.kt b/core/domain/src/main/java/com/vproject/texttoimage/core/domain/GetGeneratedPromptListUseCase.kt new file mode 100644 index 0000000..8121be7 --- /dev/null +++ b/core/domain/src/main/java/com/vproject/texttoimage/core/domain/GetGeneratedPromptListUseCase.kt @@ -0,0 +1,17 @@ +package com.vproject.texttoimage.core.domain + +import com.vproject.texttoimage.core.data.repository.image.ImageRepository +import com.vproject.texttoimage.core.model.data.PromptData +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +/** + * A use case which returns the generated prompt list. + */ +class GetGeneratedPromptListUseCase @Inject constructor( + private val imageRepository: ImageRepository +) { + operator fun invoke(): Flow> { + return imageRepository.getGeneratedPromptList() + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/vproject/texttoimage/core/domain/GetTopTrendingListUseCase.kt b/core/domain/src/main/java/com/vproject/texttoimage/core/domain/GetTopTrendingListUseCase.kt new file mode 100644 index 0000000..a4869f5 --- /dev/null +++ b/core/domain/src/main/java/com/vproject/texttoimage/core/domain/GetTopTrendingListUseCase.kt @@ -0,0 +1,17 @@ +package com.vproject.texttoimage.core.domain + +import com.vproject.texttoimage.core.data.repository.image.ImageRepository +import com.vproject.texttoimage.core.model.data.PromptData +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +/** + * A use case which returns the top trending list. + */ +class GetTopTrendingListUseCase @Inject constructor( + private val imageRepository: ImageRepository +) { + operator fun invoke(): Flow> { + return imageRepository.getTopTrendingPromptList() + } +} \ No newline at end of file diff --git a/core/model/src/main/java/com/vproject/texttoimage/core/model/data/PromptData.kt b/core/model/src/main/java/com/vproject/texttoimage/core/model/data/PromptData.kt new file mode 100644 index 0000000..9925bd6 --- /dev/null +++ b/core/model/src/main/java/com/vproject/texttoimage/core/model/data/PromptData.kt @@ -0,0 +1,11 @@ +package com.vproject.texttoimage.core.model.data + +/** + * External data layer representation of a Text To Image Prompt + */ +data class PromptData( + val id: Long = 0, + val status: PromptStatus, + val imageUrl: String? = null, + val content: String = "" +) \ No newline at end of file diff --git a/core/model/src/main/java/com/vproject/texttoimage/core/model/data/PromptStatus.kt b/core/model/src/main/java/com/vproject/texttoimage/core/model/data/PromptStatus.kt new file mode 100644 index 0000000..45f63ec --- /dev/null +++ b/core/model/src/main/java/com/vproject/texttoimage/core/model/data/PromptStatus.kt @@ -0,0 +1,8 @@ +package com.vproject.texttoimage.core.model.data + +enum class PromptStatus(val value: String) { + Failed("failed"), + Error("error"), + Processing("processing"), + Success("success"), +} \ No newline at end of file diff --git a/core/model/src/main/java/com/vproject/texttoimage/core/model/data/Style.kt b/core/model/src/main/java/com/vproject/texttoimage/core/model/data/Style.kt index 0f1a68b..333b6ef 100644 --- a/core/model/src/main/java/com/vproject/texttoimage/core/model/data/Style.kt +++ b/core/model/src/main/java/com/vproject/texttoimage/core/model/data/Style.kt @@ -7,5 +7,6 @@ data class Style( val id: String, val name: String, val imageUrl: String, - val fullDescription: String + val prompt: String, + val negativePrompt: String ) \ No newline at end of file diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index c991494..944b91a 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -23,6 +23,7 @@ android { } dependencies { + implementation(project(":core:model")) implementation(libs.kotlinx.coroutines.android) implementation(libs.okhttp.logging) implementation(libs.retrofit.core) diff --git a/core/network/src/main/java/com/vproject/texttoimage/core/network/TextToImageNetworkDataSource.kt b/core/network/src/main/java/com/vproject/texttoimage/core/network/TextToImageNetworkDataSource.kt index fbfcea0..939abb4 100644 --- a/core/network/src/main/java/com/vproject/texttoimage/core/network/TextToImageNetworkDataSource.kt +++ b/core/network/src/main/java/com/vproject/texttoimage/core/network/TextToImageNetworkDataSource.kt @@ -1,8 +1,28 @@ package com.vproject.texttoimage.core.network +import com.vproject.texttoimage.core.network.model.QueuedImageResponseBody import com.vproject.texttoimage.core.network.model.TextToImageResponseBody import com.vproject.texttoimage.core.network.model.TextToImageRequestBody interface TextToImageNetworkDataSource { - suspend fun postTextToImage(textToImageRequestBody: TextToImageRequestBody): TextToImageResponseBody -} \ No newline at end of file + /** + * Method to request generates and returns an image from a prompt API. + * + * @param prompt Text prompt with description of the things you want in the image to be generated. + * @param negativePrompt Items you don't want in the image. + * + * @return text to image response body. + */ + suspend fun postTextToImage(prompt: String, negativePrompt: String): TextToImageResponseBody + + /** + * Method to request queued images from stable diffusion API. + * Usually more complex image generation requests take more time for processing. + * Such requests are being queued for processing and the output images are retrievable after some time. + * + * @param id The ID returned together with the image URL in the response upon its generation. + * + * @return queued image response body. + */ + suspend fun fetchQueuedImage(id: Long): QueuedImageResponseBody +} diff --git a/core/network/src/main/java/com/vproject/texttoimage/core/network/di/DataSourceModule.kt b/core/network/src/main/java/com/vproject/texttoimage/core/network/di/DataSourceModule.kt index 55b23bc..713184e 100644 --- a/core/network/src/main/java/com/vproject/texttoimage/core/network/di/DataSourceModule.kt +++ b/core/network/src/main/java/com/vproject/texttoimage/core/network/di/DataSourceModule.kt @@ -9,7 +9,7 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) -interface DataSourceModule { +internal interface DataSourceModule { @Binds fun RetrofitTextToImageNetwork.binds(): TextToImageNetworkDataSource } \ No newline at end of file diff --git a/core/network/src/main/java/com/vproject/texttoimage/core/network/di/NetworkModule.kt b/core/network/src/main/java/com/vproject/texttoimage/core/network/di/NetworkModule.kt index a8874c3..93d2c9d 100644 --- a/core/network/src/main/java/com/vproject/texttoimage/core/network/di/NetworkModule.kt +++ b/core/network/src/main/java/com/vproject/texttoimage/core/network/di/NetworkModule.kt @@ -1,5 +1,6 @@ package com.vproject.texttoimage.core.network.di +import com.vproject.texttoimage.core.network.interceptor.TimeoutInterceptor import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -12,14 +13,11 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -object NetworkModule { +internal object NetworkModule { @Provides @Singleton fun okHttpCallFactory(): Call.Factory = OkHttpClient.Builder() - .connectTimeout(1, TimeUnit.MINUTES) - .callTimeout(1, TimeUnit.MINUTES) - .writeTimeout(1, TimeUnit.MINUTES) - .readTimeout(1, TimeUnit.MINUTES) + .addInterceptor(TimeoutInterceptor()) .addInterceptor(HttpLoggingInterceptor().apply { setLevel(HttpLoggingInterceptor.Level.BODY) }) diff --git a/core/network/src/main/java/com/vproject/texttoimage/core/network/interceptor/TimeoutInterceptor.kt b/core/network/src/main/java/com/vproject/texttoimage/core/network/interceptor/TimeoutInterceptor.kt new file mode 100644 index 0000000..9c9871c --- /dev/null +++ b/core/network/src/main/java/com/vproject/texttoimage/core/network/interceptor/TimeoutInterceptor.kt @@ -0,0 +1,26 @@ +package com.vproject.texttoimage.core.network.interceptor + +import okhttp3.Interceptor +import okhttp3.Response +import java.util.concurrent.TimeUnit + +internal class TimeoutInterceptor : Interceptor { + private companion object { + private const val DEFAULT_CONNECT_TIMEOUT_MS = 120000 + private const val DEFAULT_READ_TIMEOUT_MS = 120000 + private const val DEFAULT_WRITE_TIMEOUT_MS = 120000 + } + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val connectTimeoutRequest = request.header("CONNECT_TIMEOUT") + val readTimeoutRequest = request.header("READ_TIMEOUT") + val writeTimeoutRequest = request.header("WRITE_TIMEOUT") + + return chain + .withConnectTimeout(if (connectTimeoutRequest.isNullOrEmpty()) DEFAULT_CONNECT_TIMEOUT_MS else Integer.valueOf(connectTimeoutRequest), TimeUnit.MILLISECONDS) + .withReadTimeout(if (readTimeoutRequest.isNullOrEmpty()) DEFAULT_READ_TIMEOUT_MS else Integer.valueOf(readTimeoutRequest), TimeUnit.MILLISECONDS) + .withWriteTimeout(if (writeTimeoutRequest.isNullOrEmpty()) DEFAULT_WRITE_TIMEOUT_MS else Integer.valueOf(writeTimeoutRequest), TimeUnit.MILLISECONDS) + .proceed(request) + } +} \ No newline at end of file diff --git a/core/network/src/main/java/com/vproject/texttoimage/core/network/model/QueuedImageRequestBody.kt b/core/network/src/main/java/com/vproject/texttoimage/core/network/model/QueuedImageRequestBody.kt new file mode 100644 index 0000000..5912607 --- /dev/null +++ b/core/network/src/main/java/com/vproject/texttoimage/core/network/model/QueuedImageRequestBody.kt @@ -0,0 +1,6 @@ +package com.vproject.texttoimage.core.network.model + +data class QueuedImageRequestBody( + // Your API Key used for request authorization + val key: String, +) \ No newline at end of file diff --git a/core/network/src/main/java/com/vproject/texttoimage/core/network/model/QueuedImageResponseBody.kt b/core/network/src/main/java/com/vproject/texttoimage/core/network/model/QueuedImageResponseBody.kt new file mode 100644 index 0000000..075df42 --- /dev/null +++ b/core/network/src/main/java/com/vproject/texttoimage/core/network/model/QueuedImageResponseBody.kt @@ -0,0 +1,14 @@ +package com.vproject.texttoimage.core.network.model + +import com.vproject.texttoimage.core.model.data.PromptData +import com.vproject.texttoimage.core.model.data.PromptStatus + +data class QueuedImageResponseBody( + val id: Long, + val status: String, + val output: List +) + +fun QueuedImageResponseBody.toPromptData(): PromptData = + PromptData(id, PromptStatus.values().find { it.value == status } + ?: throw IllegalArgumentException("Illegal Argument Exception"), output.first(), "") \ No newline at end of file diff --git a/core/network/src/main/java/com/vproject/texttoimage/core/network/model/ResultWrapper.kt b/core/network/src/main/java/com/vproject/texttoimage/core/network/model/ResultWrapper.kt new file mode 100644 index 0000000..72a3341 --- /dev/null +++ b/core/network/src/main/java/com/vproject/texttoimage/core/network/model/ResultWrapper.kt @@ -0,0 +1,38 @@ +package com.vproject.texttoimage.core.network.model + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import retrofit2.HttpException +import java.io.IOException +import java.net.SocketTimeoutException + +sealed class ResultWrapper { + data class Success(val data: ResponseType) : + ResultWrapper() + + data class Error(val message: ErrorType) : ResultWrapper() +} + +enum class ErrorType { + HTTP, + IO, // IO + TIMEOUT, // Socket + UNKNOWN +} + +suspend fun safeApiCall( + dispatcher: CoroutineDispatcher, + apiCall: suspend () -> ResponseType +): ResultWrapper { + return try { + val response = withContext(dispatcher) { apiCall.invoke() } + ResultWrapper.Success(response) + } catch (exception: Exception) { + when (exception) { + is HttpException -> ResultWrapper.Error(ErrorType.HTTP) + is SocketTimeoutException -> ResultWrapper.Error(ErrorType.TIMEOUT) + is IOException -> ResultWrapper.Error(ErrorType.IO) + else -> ResultWrapper.Error(ErrorType.UNKNOWN) + } + } +} \ No newline at end of file diff --git a/core/network/src/main/java/com/vproject/texttoimage/core/network/model/TextToImageRequestBody.kt b/core/network/src/main/java/com/vproject/texttoimage/core/network/model/TextToImageRequestBody.kt index 5ec542b..a2efe8e 100644 --- a/core/network/src/main/java/com/vproject/texttoimage/core/network/model/TextToImageRequestBody.kt +++ b/core/network/src/main/java/com/vproject/texttoimage/core/network/model/TextToImageRequestBody.kt @@ -1,22 +1,40 @@ package com.vproject.texttoimage.core.network.model data class TextToImageRequestBody( - val key: String = "", - val prompt: String = "", - val negative_prompt: String? = null, - val width: String = "1024", - val height: String = "1024", + // Your API Key used for request authorization + val key: String, + // Text prompt with description of the things you want in the image to be generated. + val prompt: String, + // Items you don't want in the image. + val negative_prompt: String = "", + // Max Width: 1024x1024. + val width: String = "512", + // Max Height: 1024x1024. + val height: String = "512", + // Number of images to be returned in response. The maximum value is 4. val samples: String = "1", - val num_inference_steps: String = "20", + // Number of denoising steps. Available values: 21, 31, 41, 51. + val num_inference_steps: String = "21", + // A checker for NSFW images. If such an image is detected, it will be replaced by a blank image. val safety_checker: String = "no", + // Enhance prompts for better results; default: yes, options: yes/no. val enhance_prompt: String = "yes", + // Seed is used to reproduce results, same seed will give you same image in return again. Pass null for a random number. val seed: Int? = null, + // Scale for classifier-free guidance (minimum: 1; maximum: 20). val guidance_scale: Double = 7.5, + // Allow multi lingual prompt to generate images. Use "no" for the default English. val multi_lingual: String = "yes", + // Set this parameter to "yes" to generate a panorama image. val panorama: String = "no", - val self_attention: String = "no", + // If you want a high quality image, set this parameter to "yes". In this case the image generation will take more time. + val self_attention: String = "yes", + // Set this parameter to "yes" if you want to upscale the given image resolution two times (2x). val upscale: String = "yes", + // This is used to pass an embeddings model (embeddings_model_id). val embeddings_model: String = "embeddings_model_id", + // Set an URL to get a POST API call once the image generation is complete. val webhook: String? = null, + // This ID is returned in the response to the webhook API call. This will be used to identify the webhook request. val track_id: String? = null ) \ No newline at end of file diff --git a/core/network/src/main/java/com/vproject/texttoimage/core/network/model/TextToImageResponseBody.kt b/core/network/src/main/java/com/vproject/texttoimage/core/network/model/TextToImageResponseBody.kt index 3c4f43b..50599bc 100644 --- a/core/network/src/main/java/com/vproject/texttoimage/core/network/model/TextToImageResponseBody.kt +++ b/core/network/src/main/java/com/vproject/texttoimage/core/network/model/TextToImageResponseBody.kt @@ -1,8 +1,19 @@ package com.vproject.texttoimage.core.network.model +import com.vproject.texttoimage.core.model.data.PromptData +import com.vproject.texttoimage.core.model.data.PromptStatus + data class TextToImageResponseBody( - val id: Long? = null, + val id: Long, val status: String, - val generateTime: Double, - val output: List -) \ No newline at end of file + val generateTime: Double?, + val output: List? +) + +fun TextToImageResponseBody.toPromptData(): PromptData = + PromptData( + id, + PromptStatus.values().find { it.value == status } + ?: throw IllegalArgumentException("Illegal Argument Exception"), + output?.firstOrNull(), + "") \ No newline at end of file diff --git a/core/network/src/main/java/com/vproject/texttoimage/core/network/retrofit/RetrofitTextToImageApi.kt b/core/network/src/main/java/com/vproject/texttoimage/core/network/retrofit/RetrofitTextToImageApi.kt index 9475b9a..c8de00f 100644 --- a/core/network/src/main/java/com/vproject/texttoimage/core/network/retrofit/RetrofitTextToImageApi.kt +++ b/core/network/src/main/java/com/vproject/texttoimage/core/network/retrofit/RetrofitTextToImageApi.kt @@ -1,16 +1,25 @@ package com.vproject.texttoimage.core.network.retrofit +import com.vproject.texttoimage.core.network.model.QueuedImageRequestBody +import com.vproject.texttoimage.core.network.model.QueuedImageResponseBody import com.vproject.texttoimage.core.network.model.TextToImageResponseBody import com.vproject.texttoimage.core.network.model.TextToImageRequestBody import retrofit2.http.Body import retrofit2.http.POST +import retrofit2.http.Path /** * Retrofit API declaration for Text To Image Network API */ -interface RetrofitTextToImageApi { +internal interface RetrofitTextToImageApi { @POST(value = "api/v3/text2img") suspend fun postTextToImage( @Body textToImageRequestBody: TextToImageRequestBody, ): TextToImageResponseBody + + @POST(value = "api/v3/fetch/{id}") + suspend fun fetchQueuedImage( + @Path("id") id: Long, + @Body textToImageRequestBody: QueuedImageRequestBody, + ): QueuedImageResponseBody } \ No newline at end of file diff --git a/core/network/src/main/java/com/vproject/texttoimage/core/network/retrofit/RetrofitTextToImageNetwork.kt b/core/network/src/main/java/com/vproject/texttoimage/core/network/retrofit/RetrofitTextToImageNetwork.kt index ca57113..c9d9372 100644 --- a/core/network/src/main/java/com/vproject/texttoimage/core/network/retrofit/RetrofitTextToImageNetwork.kt +++ b/core/network/src/main/java/com/vproject/texttoimage/core/network/retrofit/RetrofitTextToImageNetwork.kt @@ -2,6 +2,8 @@ package com.vproject.texttoimage.core.network.retrofit import com.vproject.texttoimage.core.network.BuildConfig import com.vproject.texttoimage.core.network.TextToImageNetworkDataSource +import com.vproject.texttoimage.core.network.model.QueuedImageRequestBody +import com.vproject.texttoimage.core.network.model.QueuedImageResponseBody import com.vproject.texttoimage.core.network.model.TextToImageResponseBody import com.vproject.texttoimage.core.network.model.TextToImageRequestBody import okhttp3.Call @@ -14,11 +16,11 @@ import javax.inject.Singleton * [Retrofit] backed [TextToImageNetworkDataSource] */ @Singleton -class RetrofitTextToImageNetwork @Inject constructor(okhttpCallFactory: Call.Factory) : +internal class RetrofitTextToImageNetwork @Inject constructor(okhttpCallFactory: Call.Factory) : TextToImageNetworkDataSource { companion object { private const val STABLE_DIFFUSION_BASE_URL = "https://stablediffusionapi.com" - private val STABLE_DIFFUSION_API_KEY = BuildConfig.STABLE_DIFFUSION_API_KEY + private const val STABLE_DIFFUSION_API_KEY = BuildConfig.STABLE_DIFFUSION_API_KEY } private val networkApi = Retrofit.Builder() @@ -28,8 +30,34 @@ class RetrofitTextToImageNetwork @Inject constructor(okhttpCallFactory: Call.Fac .build() .create(RetrofitTextToImageApi::class.java) - override suspend fun postTextToImage(textToImageRequestBody: TextToImageRequestBody): TextToImageResponseBody = + /** + * Method to request generates and returns an image from a prompt API. + * + * @param prompt Text prompt with description of the things you want in the image to be generated. + * @param negativePrompt Items you don't want in the image. + * + * @return text to image response body. + */ + override suspend fun postTextToImage(prompt: String, negativePrompt: String): TextToImageResponseBody = networkApi.postTextToImage( - textToImageRequestBody.copy(key = STABLE_DIFFUSION_API_KEY) + TextToImageRequestBody( + key = STABLE_DIFFUSION_API_KEY, + prompt = prompt, + negative_prompt = negativePrompt + ) + ) + + /** + * Method to request queued images from stable diffusion API. + * Usually more complex image generation requests take more time for processing. + * Such requests are being queued for processing and the output images are retrievable after some time. + * + * @param id The ID returned together with the image URL in the response upon its generation. + * + * @return queued image response body. + */ + override suspend fun fetchQueuedImage(id: Long): QueuedImageResponseBody = + networkApi.fetchQueuedImage( + id, QueuedImageRequestBody(key = STABLE_DIFFUSION_API_KEY) ) } \ No newline at end of file diff --git a/core/testing/src/main/java/com/vproject/texttoimage/core/testing/repository/TestImageRepository.kt b/core/testing/src/main/java/com/vproject/texttoimage/core/testing/repository/TestImageRepository.kt index bc9291b..d55f6a9 100644 --- a/core/testing/src/main/java/com/vproject/texttoimage/core/testing/repository/TestImageRepository.kt +++ b/core/testing/src/main/java/com/vproject/texttoimage/core/testing/repository/TestImageRepository.kt @@ -1,17 +1,31 @@ package com.vproject.texttoimage.core.testing.repository import com.vproject.texttoimage.core.data.repository.image.ImageRepository +import com.vproject.texttoimage.core.model.data.PromptData +import com.vproject.texttoimage.core.model.data.PromptStatus import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow class TestImageRepository : ImageRepository { - override suspend fun generateImage(prompt: String): Flow { + override suspend fun generateImage(prompt: String, negativePrompt: String): Flow { return flow { if (prompt.isEmpty()) { - emit("") + emit(PromptData(0, PromptStatus.Processing, "", "")) } else { - emit("https://cdn.stablediffusionapi.com/generations/a8fcf169-b467-41e4-924b-6c168ed73a71-0.png") + emit(PromptData(0, PromptStatus.Success, "", "")) } } } + + override suspend fun fetchQueuedImage(id: Long): Flow { + TODO("Not yet implemented") + } + + override fun getGeneratedPromptList(): Flow> { + TODO("Not yet implemented") + } + + override fun getTopTrendingPromptList(): Flow> { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/docs/images/android-text-to-image-showcase.gif b/docs/images/android-text-to-image-showcase.gif deleted file mode 100644 index bb38c6f..0000000 Binary files a/docs/images/android-text-to-image-showcase.gif and /dev/null differ diff --git a/docs/images/showcase_v1.gif b/docs/images/showcase_v1.gif deleted file mode 100644 index 51b1f2c..0000000 Binary files a/docs/images/showcase_v1.gif and /dev/null differ diff --git a/docs/images/showcase_v2.gif b/docs/images/showcase_v2.gif new file mode 100644 index 0000000..2b433f9 Binary files /dev/null and b/docs/images/showcase_v2.gif differ diff --git a/feature/explore/src/main/java/com/vproject/texttoimage/feature/explore/ExploreScreen.kt b/feature/explore/src/main/java/com/vproject/texttoimage/feature/explore/ExploreScreen.kt deleted file mode 100644 index edd7277..0000000 --- a/feature/explore/src/main/java/com/vproject/texttoimage/feature/explore/ExploreScreen.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.vproject.texttoimage.feature.explore - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource - -@Composable -internal fun ExploreRoute( - modifier: Modifier = Modifier) { - ExploreScreen(modifier = modifier) -} - -@Composable -internal fun ExploreScreen(modifier: Modifier = Modifier, -) { - Surface( - modifier = modifier - .fillMaxSize() - .background(Color.Black) - ) { - Text(text = "Dummy UI") - } -} \ No newline at end of file diff --git a/feature/explore/src/main/java/com/vproject/texttoimage/feature/explore/navigation/ExploreNavigation.kt b/feature/explore/src/main/java/com/vproject/texttoimage/feature/explore/navigation/ExploreNavigation.kt deleted file mode 100644 index 9b5bfa6..0000000 --- a/feature/explore/src/main/java/com/vproject/texttoimage/feature/explore/navigation/ExploreNavigation.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.vproject.texttoimage.feature.explore.navigation - -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavOptions -import androidx.navigation.compose.composable -import com.vproject.texttoimage.feature.explore.ExploreRoute - -const val exploreRoute = "explore_route" - -fun NavController.navigateToExplore(navOptions: NavOptions? = null) { - this.navigate(exploreRoute, navOptions) -} - -fun NavGraphBuilder.exploreScreen() { - composable(route = exploreRoute) { - ExploreRoute() - } -} \ No newline at end of file diff --git a/feature/explore/src/main/res/values/strings.xml b/feature/explore/src/main/res/values/strings.xml deleted file mode 100644 index 4cd3f75..0000000 --- a/feature/explore/src/main/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - Explore - \ No newline at end of file diff --git a/feature/explore/build.gradle.kts b/feature/gallery/build.gradle.kts similarity index 76% rename from feature/explore/build.gradle.kts rename to feature/gallery/build.gradle.kts index 20a1768..fb706f6 100644 --- a/feature/explore/build.gradle.kts +++ b/feature/gallery/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } android { - namespace = "com.vproject.texttoimage.feature.explore" + namespace = "com.vproject.texttoimage.feature.gallery" } dependencies { diff --git a/feature/explore/src/main/AndroidManifest.xml b/feature/gallery/src/main/AndroidManifest.xml similarity index 100% rename from feature/explore/src/main/AndroidManifest.xml rename to feature/gallery/src/main/AndroidManifest.xml diff --git a/feature/gallery/src/main/java/com/vproject/texttoimage/feature/gallery/GalleryScreen.kt b/feature/gallery/src/main/java/com/vproject/texttoimage/feature/gallery/GalleryScreen.kt new file mode 100644 index 0000000..5a52719 --- /dev/null +++ b/feature/gallery/src/main/java/com/vproject/texttoimage/feature/gallery/GalleryScreen.kt @@ -0,0 +1,123 @@ +package com.vproject.texttoimage.feature.gallery + +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vproject.texttoimage.core.designsystem.component.DynamicAsyncImage +import com.vproject.texttoimage.core.model.data.PromptData + +@Composable +internal fun GalleryRoute( + viewModel: GalleryViewModel = hiltViewModel(), + onPromptItemClick: (promptContent: String, imageUrl: String) -> Unit +) { + val galleryUiState by viewModel.galleryUiState.collectAsStateWithLifecycle() + + GalleryScreen(galleryUiState = galleryUiState, onPromptItemClick = onPromptItemClick) +} + +@Composable +internal fun GalleryScreen( + modifier: Modifier = Modifier, + galleryUiState: GalleryUiState, + onPromptItemClick: (promptContent: String, imageUrl: String) -> Unit +) { + Column { + if (galleryUiState is GalleryUiState.Success) { + LazyVerticalGrid( + modifier = modifier.padding(start = 10.dp, end = 10.dp), + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + item(span = { GridItemSpan(2) }) { + Text( + text = "Generated Image", + style = TextStyle( + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Bold, + fontSize = 20.sp + ), + ) + Spacer(Modifier.height(10.dp)) + } + + items(galleryUiState.generatedPromptList) { promptData -> + GeneratedPromptItem( + promptData = promptData, + onItemClick = onPromptItemClick + ) + } + } + } + } +} + +@Composable +private fun GeneratedPromptItem( + modifier: Modifier = Modifier, + promptData: PromptData, + onItemClick: (promptContent: String, imageUrl: String) -> Unit +) { + Box( + modifier + .fillMaxWidth() + .height(230.dp) + .clickable { + onItemClick(promptData.content, promptData.imageUrl ?: "") + } + ) { + DynamicAsyncImage( + imageUrl = promptData.imageUrl ?: "", + contentDescription = null, + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.medium), + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .height(42.dp) + .padding(start = 5.dp, end = 5.dp, bottom = 5.dp) + .align(Alignment.BottomEnd) + ) { + Text( + style = TextStyle( + color = MaterialTheme.colorScheme.secondary, + fontWeight = FontWeight.Normal, + fontSize = 14.sp + ), + modifier = Modifier.fillMaxWidth(), + text = promptData.content, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } +} \ No newline at end of file diff --git a/feature/gallery/src/main/java/com/vproject/texttoimage/feature/gallery/GalleryUiState.kt b/feature/gallery/src/main/java/com/vproject/texttoimage/feature/gallery/GalleryUiState.kt new file mode 100644 index 0000000..eb24239 --- /dev/null +++ b/feature/gallery/src/main/java/com/vproject/texttoimage/feature/gallery/GalleryUiState.kt @@ -0,0 +1,8 @@ +package com.vproject.texttoimage.feature.gallery + +import com.vproject.texttoimage.core.model.data.PromptData + +internal sealed interface GalleryUiState { + object Loading: GalleryUiState + data class Success(val generatedPromptList: List): GalleryUiState +} \ No newline at end of file diff --git a/feature/gallery/src/main/java/com/vproject/texttoimage/feature/gallery/GalleryViewModel.kt b/feature/gallery/src/main/java/com/vproject/texttoimage/feature/gallery/GalleryViewModel.kt new file mode 100644 index 0000000..7a7cc06 --- /dev/null +++ b/feature/gallery/src/main/java/com/vproject/texttoimage/feature/gallery/GalleryViewModel.kt @@ -0,0 +1,24 @@ +package com.vproject.texttoimage.feature.gallery + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.vproject.texttoimage.core.domain.GetGeneratedPromptListUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@HiltViewModel +internal class GalleryViewModel @Inject constructor( + getGeneratedPromptListUseCase: GetGeneratedPromptListUseCase +) : ViewModel() { + val galleryUiState: StateFlow = + getGeneratedPromptListUseCase().map(GalleryUiState::Success) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = GalleryUiState.Loading + ) +} \ No newline at end of file diff --git a/feature/gallery/src/main/java/com/vproject/texttoimage/feature/gallery/navigation/GalleryNavigation.kt b/feature/gallery/src/main/java/com/vproject/texttoimage/feature/gallery/navigation/GalleryNavigation.kt new file mode 100644 index 0000000..e10b9ba --- /dev/null +++ b/feature/gallery/src/main/java/com/vproject/texttoimage/feature/gallery/navigation/GalleryNavigation.kt @@ -0,0 +1,21 @@ +package com.vproject.texttoimage.feature.gallery.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.vproject.texttoimage.feature.gallery.GalleryRoute + +const val galleryRoute = "gallery_route" + +fun NavController.navigateToGallery(navOptions: NavOptions? = null) { + this.navigate(galleryRoute, navOptions) +} + +fun NavGraphBuilder.galleryScreen( + onPromptItemClick: (promptContent: String, imageUrl: String) -> Unit +) { + composable(route = galleryRoute) { + GalleryRoute(onPromptItemClick = onPromptItemClick) + } +} \ No newline at end of file diff --git a/feature/gallery/src/main/res/values/strings.xml b/feature/gallery/src/main/res/values/strings.xml new file mode 100644 index 0000000..fede713 --- /dev/null +++ b/feature/gallery/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Gallery + \ No newline at end of file diff --git a/feature/generate/src/main/kotlin/com/vproject/texttoimage/feature/generate/GenerateScreen.kt b/feature/generate/src/main/kotlin/com/vproject/texttoimage/feature/generate/GenerateScreen.kt index 9079bfd..ee93c51 100644 --- a/feature/generate/src/main/kotlin/com/vproject/texttoimage/feature/generate/GenerateScreen.kt +++ b/feature/generate/src/main/kotlin/com/vproject/texttoimage/feature/generate/GenerateScreen.kt @@ -6,6 +6,7 @@ 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 @@ -15,7 +16,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.shape.CircleShape @@ -28,39 +31,33 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import coil.compose.AsyncImage -import coil.request.ImageRequest +import com.vproject.texttoimage.core.designsystem.component.DynamicAsyncImage import com.vproject.texttoimage.core.designsystem.component.TextToImageFilledButton -import com.vproject.texttoimage.core.designsystem.component.TextToImageTextButton import com.vproject.texttoimage.core.designsystem.component.TextToImageTextField import com.vproject.texttoimage.core.designsystem.icon.TextToImageIcons import com.vproject.texttoimage.core.model.data.FavorableStyle - -private val randomList = listOf( - "Portrait of Harry Potter cooking cheeseburger", - "Dwayne Johnson as Superman, realistic portrait", - "Colin Farrell as the president of the USA", - "Happy charming googly-eyed potato walking around a cardboard diorama town chatting" -) +import com.vproject.texttoimage.core.model.data.PromptData +import kotlinx.coroutines.launch @Composable internal fun GenerateRoute( @@ -88,6 +85,7 @@ internal fun GenerateScreen( GenerateContent( modifier.testTag(GenerateTestTags.GenerateContent), styleList = generateUiState.styles, + topTrendingList = generateUiState.topTrendingList, onToggleFavoriteStyleItem = onToggleFavoriteStyleItem, onGenerateButtonClicked = onGenerateButtonClicked ) @@ -98,64 +96,80 @@ internal fun GenerateScreen( private fun GenerateContent( modifier: Modifier = Modifier, styleList: List, + topTrendingList: List, onToggleFavoriteStyleItem: (styleId: String, isFavorite: Boolean) -> Unit, onGenerateButtonClicked: (prompt: String, selectedStyleId: String) -> Unit ) { var promptValue by remember { mutableStateOf("") } var selectedStyleId by remember { mutableStateOf("1") } - Column(modifier.padding(start = 10.dp, end = 10.dp)) { - TextToImageTextButton( - modifier = Modifier.height(35.dp), - onClick = { - promptValue = randomList.filter { it != promptValue }.random() - }, - text = { + val listState = rememberLazyGridState() + val coroutineScope = rememberCoroutineScope() + + LazyVerticalGrid( + modifier = modifier.padding(start = 10.dp, end = 10.dp), + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + state = listState + ) { + item(span = { GridItemSpan(2) }) { + Column { + GeneratePromptTextField( + modifier = Modifier.testTag(GenerateTestTags.GeneratePromptTextField), + value = promptValue, + onValueChange = { promptValue = it }, + onClearContentClick = { promptValue = ""} + ) + + Spacer(Modifier.height(10.dp)) Text( - text = "Surprise Me", + text = "Select your style", style = TextStyle( color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.SemiBold, - fontSize = 16.sp - ) + fontWeight = FontWeight.Bold, + fontSize = 20.sp + ), ) - }, - leadingIcon = { - Icon( - imageVector = TextToImageIcons.RoundedAutoFixNormal, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, + Spacer(Modifier.height(10.dp)) + SelectStyleList( + modifier = Modifier.fillMaxWidth(), + styleList = styleList, + selectedStyleId = selectedStyleId, + onToggleFavoriteStyleItem = onToggleFavoriteStyleItem, + onStyleSelected = { styleId -> selectedStyleId = styleId } + ) + Spacer(Modifier.height(10.dp)) + GenerateButton( + modifier = Modifier.testTag(GenerateTestTags.GenerateImageButton), + enabled = promptValue.isNotEmpty() && selectedStyleId.isNotEmpty(), + onClick = { onGenerateButtonClicked(promptValue, selectedStyleId) } ) - }, - ) - GeneratePromptTextField( - onValueChange = { promptValue = it }, value = promptValue, - modifier = Modifier.testTag(GenerateTestTags.GeneratePromptTextField) - ) - Spacer(Modifier.height(10.dp)) - Text( - text = "Select Style", - style = TextStyle( - color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.Bold, - fontSize = 20.sp - ), - ) - Spacer(Modifier.height(10.dp)) - SelectStyleList( - modifier = Modifier.fillMaxWidth(), - styleList = styleList, - selectedStyleId = selectedStyleId, - onToggleFavoriteStyleItem = onToggleFavoriteStyleItem, - onStyleSelected = { styleId -> selectedStyleId = styleId } - ) - Spacer(Modifier.height(10.dp)) - GenerateButton( - modifier = Modifier.testTag(GenerateTestTags.GenerateImageButton), - enabled = promptValue.isNotEmpty() && selectedStyleId.isNotEmpty(), - onClick = { onGenerateButtonClicked(promptValue, selectedStyleId) } - ) + Spacer(Modifier.height(10.dp)) + Text( + text = "Top trending", + style = TextStyle( + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Bold, + fontSize = 20.sp + ), + ) + Spacer(Modifier.height(10.dp)) + } + } + + items(topTrendingList) { promptData -> + TopTrendingItem( + promptData = promptData, + onTryClick = { + promptValue = it + coroutineScope.launch { + listState.animateScrollToItem(0) + } + } + ) + } } } @@ -169,10 +183,10 @@ private fun SelectStyleList( ) { val lazyGridState = rememberLazyGridState() LazyHorizontalGrid( - modifier = modifier.height(280.dp), + modifier = modifier.height(140.dp), state = lazyGridState, horizontalArrangement = Arrangement.spacedBy(10.dp), - rows = GridCells.Fixed(2) + rows = GridCells.Fixed(1) ) { items( items = styleList, @@ -203,27 +217,27 @@ private fun StyleItem( Box( modifier .fillMaxWidth() - .border(1.dp, MaterialTheme.colorScheme.onSurface, RoundedCornerShape(10)) + .border( + 1.dp, + if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, + RoundedCornerShape(10) + ) .aspectRatio(1f) .align(Alignment.CenterHorizontally) .clickable { onStyleSelected(style.style.id) } ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(style.style.imageUrl) - .crossfade(true) - .build(), + DynamicAsyncImage( + imageUrl = style.style.imageUrl, contentDescription = null, - contentScale = ContentScale.Crop, modifier = Modifier .fillMaxSize() .clip(MaterialTheme.shapes.medium), ) - if (isSelected) { + if (!isSelected) { Box( modifier = Modifier .fillMaxSize() - .alpha(0.4f) + .alpha(0.7f) .background(MaterialTheme.colorScheme.onSurface, RoundedCornerShape(10)) .clip(MaterialTheme.shapes.medium) ) @@ -239,6 +253,7 @@ private fun StyleItem( Text( text = style.style.name, + textAlign = TextAlign.Center, style = TextStyle( color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.SemiBold, @@ -266,7 +281,7 @@ fun FavoriteStyleButton( Icon( imageVector = if (isFavorite) TextToImageIcons.FilledFavorite else TextToImageIcons.OutlinedFavorite, contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, + tint = MaterialTheme.colorScheme.primary, modifier = Modifier .shadow( elevation = 1.dp, @@ -283,10 +298,12 @@ fun FavoriteStyleButton( @Composable private fun GeneratePromptTextField( - onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, value: String, - modifier: Modifier = Modifier -) { + onValueChange: (String) -> Unit, + onClearContentClick: () -> Unit + + ) { TextToImageTextField( onValueChange = onValueChange, textStyle = TextStyle(color = MaterialTheme.colorScheme.onSurface, fontSize = 16.sp), @@ -295,17 +312,20 @@ private fun GeneratePromptTextField( subHint = stringResource(id = R.string.generate_sub_hint), leadingIcon = { Icon( - tint = MaterialTheme.colorScheme.onSurface, + tint = MaterialTheme.colorScheme.primary, imageVector = TextToImageIcons.DefaultHistory, contentDescription = null, ) }, trailingIcon = { - Icon( - tint = MaterialTheme.colorScheme.onSurface, - imageVector = TextToImageIcons.DefaultClose, - contentDescription = null - ) + if (value.isNotEmpty()) { + Icon( + modifier = Modifier.clickable { onClearContentClick() }, + tint = MaterialTheme.colorScheme.primary, + imageVector = TextToImageIcons.DefaultClose, + contentDescription = null + ) + } }, modifier = modifier.fillMaxWidth() ) @@ -328,6 +348,68 @@ private fun GenerateButton(modifier: Modifier = Modifier, enabled: Boolean, onCl ) } +@Composable +private fun TopTrendingItem( + modifier: Modifier = Modifier, + promptData: PromptData, + onTryClick: (promptContent: String) -> Unit = {} +) { + Box( + modifier + .fillMaxWidth() + .height(230.dp) + ) { + DynamicAsyncImage( + imageUrl = promptData.imageUrl ?: "", + contentDescription = null, + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.medium), + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .height(42.dp) + .padding(start = 5.dp, end = 5.dp, bottom = 5.dp) + .align(Alignment.BottomEnd) + ) { + Text( + style = TextStyle( + color = MaterialTheme.colorScheme.secondary, + fontWeight = FontWeight.Normal, + fontSize = 14.sp + ), + modifier = Modifier.weight(8f), + text = promptData.content, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + + Box( + modifier = Modifier + .weight(2f) + .height(35.dp) + .clip(RoundedCornerShape(10.dp)) + .background(MaterialTheme.colorScheme.primary) + .clickable { + onTryClick(promptData.content) + }) { + Text( + modifier = Modifier.align(Alignment.Center), + style = TextStyle( + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.secondary, + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp + ), + text = "Try", + ) + } + } + } +} + internal object GenerateTestTags { const val GenerateContent = "GenerateContent" const val GeneratePromptTextField = "GeneratePromptTextField" @@ -337,7 +419,7 @@ internal object GenerateTestTags { @Preview @Composable private fun GenerateScreenPreview() { - val successGenerateUiState = GenerateUiState.Success(listOf()) + val successGenerateUiState = GenerateUiState.Success(listOf(), listOf()) GenerateScreen( generateUiState = successGenerateUiState, modifier = Modifier.fillMaxSize(), @@ -350,6 +432,7 @@ private fun GenerateScreenPreview() { private fun GenerateContentPreview() { GenerateContent( styleList = listOf(), + topTrendingList = listOf(), onToggleFavoriteStyleItem = { _, _ -> }, onGenerateButtonClicked = { _, _ -> }) } @@ -357,7 +440,7 @@ private fun GenerateContentPreview() { @Preview @Composable private fun GeneratePromptTextFieldPreview() { - GeneratePromptTextField(onValueChange = { }, value = "") + GeneratePromptTextField(value = "", onValueChange = { }, onClearContentClick = {}) } @Preview diff --git a/feature/generate/src/main/kotlin/com/vproject/texttoimage/feature/generate/GenerateUiState.kt b/feature/generate/src/main/kotlin/com/vproject/texttoimage/feature/generate/GenerateUiState.kt index e409d6c..64d0d77 100644 --- a/feature/generate/src/main/kotlin/com/vproject/texttoimage/feature/generate/GenerateUiState.kt +++ b/feature/generate/src/main/kotlin/com/vproject/texttoimage/feature/generate/GenerateUiState.kt @@ -1,8 +1,9 @@ package com.vproject.texttoimage.feature.generate import com.vproject.texttoimage.core.model.data.FavorableStyle +import com.vproject.texttoimage.core.model.data.PromptData -sealed interface GenerateUiState { +internal sealed interface GenerateUiState { object Loading: GenerateUiState - data class Success(val styles: List): GenerateUiState + data class Success(val styles: List, val topTrendingList: List): GenerateUiState } \ No newline at end of file diff --git a/feature/generate/src/main/kotlin/com/vproject/texttoimage/feature/generate/GenerateViewModel.kt b/feature/generate/src/main/kotlin/com/vproject/texttoimage/feature/generate/GenerateViewModel.kt index 149502c..d853ec0 100644 --- a/feature/generate/src/main/kotlin/com/vproject/texttoimage/feature/generate/GenerateViewModel.kt +++ b/feature/generate/src/main/kotlin/com/vproject/texttoimage/feature/generate/GenerateViewModel.kt @@ -1,27 +1,32 @@ package com.vproject.texttoimage.feature.generate -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vproject.texttoimage.core.domain.GenerateImageUseCase import com.vproject.texttoimage.core.domain.GetFavorableStyleListUseCase +import com.vproject.texttoimage.core.domain.GetTopTrendingListUseCase import com.vproject.texttoimage.core.domain.ToggleFavoriteStyleUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class GenerateViewModel @Inject constructor( +internal class GenerateViewModel @Inject constructor( getFavorableStyleListUseCase: GetFavorableStyleListUseCase, + getTopTrendingListUseCase: GetTopTrendingListUseCase, private val toggleFavoriteStyleUseCase: ToggleFavoriteStyleUseCase ) : ViewModel() { val generateUiState: StateFlow = - getFavorableStyleListUseCase() - .map(GenerateUiState::Success) + + combine( + getFavorableStyleListUseCase(), + getTopTrendingListUseCase() + ) { styleList, topTrendingList -> + GenerateUiState.Success(styleList, topTrendingList) + } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), diff --git a/feature/generate/src/main/res/values/strings.xml b/feature/generate/src/main/res/values/strings.xml index fc61d62..dc3fe99 100644 --- a/feature/generate/src/main/res/values/strings.xml +++ b/feature/generate/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ Generate - Enter a prompt - Anything • Any detail • Add your dreams + Describe your thoughts + Any detail of your image There is some problem occurred \ No newline at end of file diff --git a/feature/loading/src/main/java/com/vproject/texttoimage/feature/loading/LoadingScreen.kt b/feature/loading/src/main/java/com/vproject/texttoimage/feature/loading/LoadingScreen.kt index 01c59b5..b0e3070 100644 --- a/feature/loading/src/main/java/com/vproject/texttoimage/feature/loading/LoadingScreen.kt +++ b/feature/loading/src/main/java/com/vproject/texttoimage/feature/loading/LoadingScreen.kt @@ -27,13 +27,15 @@ import com.airbnb.lottie.compose.rememberLottieComposition internal fun LoadingRoute( modifier: Modifier = Modifier, viewModel: LoadingViewModel = hiltViewModel(), - onImageGenerated: (url: String, prompt: String, styleId: String) -> Unit = {_,_,_ ->} + onImageGenerated: (url: String, prompt: String, styleId: String) -> Unit = {_,_,_ ->}, + onError: (message: String) -> Unit = {} ) { val loadingUiState by viewModel.loadingUiState.collectAsStateWithLifecycle() LoadingScreen( loadingUiState = loadingUiState, modifier = modifier.fillMaxSize(), - onImageGenerated = onImageGenerated + onImageGenerated = onImageGenerated, + onError = onError ) } @@ -41,7 +43,8 @@ internal fun LoadingRoute( internal fun LoadingScreen( modifier: Modifier = Modifier, loadingUiState: LoadingUiState, - onImageGenerated: (url: String, prompt: String, styleId: String) -> Unit = {_,_,_ ->} + onImageGenerated: (url: String, prompt: String, styleId: String) -> Unit = {_,_,_ ->}, + onError: (message: String) -> Unit = {} ) { if (loadingUiState is LoadingUiState.Generating) { val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.loading)) @@ -51,7 +54,7 @@ internal fun LoadingScreen( horizontalAlignment = Alignment.CenterHorizontally ) { LottieAnimation( - modifier = Modifier.size(150.dp), + modifier = Modifier.size(160.dp), composition = composition, iterations = LottieConstants.IterateForever ) Text(text = "Generating...", @@ -63,6 +66,8 @@ internal fun LoadingScreen( } } else if (loadingUiState is LoadingUiState.Generated) { onImageGenerated(loadingUiState.url, loadingUiState.prompt, loadingUiState.styleId) + } else if (loadingUiState is LoadingUiState.Error) { + onError("Some unknown error just happen") } } diff --git a/feature/loading/src/main/java/com/vproject/texttoimage/feature/loading/LoadingUiState.kt b/feature/loading/src/main/java/com/vproject/texttoimage/feature/loading/LoadingUiState.kt index 9f4aa44..11e0d6e 100644 --- a/feature/loading/src/main/java/com/vproject/texttoimage/feature/loading/LoadingUiState.kt +++ b/feature/loading/src/main/java/com/vproject/texttoimage/feature/loading/LoadingUiState.kt @@ -1,6 +1,7 @@ package com.vproject.texttoimage.feature.loading -sealed interface LoadingUiState { +internal sealed interface LoadingUiState { object Generating: LoadingUiState + object Error: LoadingUiState data class Generated(val url: String, val prompt: String, val styleId: String): LoadingUiState } \ No newline at end of file diff --git a/feature/loading/src/main/java/com/vproject/texttoimage/feature/loading/LoadingViewModel.kt b/feature/loading/src/main/java/com/vproject/texttoimage/feature/loading/LoadingViewModel.kt index 9af75f0..4ab15e2 100644 --- a/feature/loading/src/main/java/com/vproject/texttoimage/feature/loading/LoadingViewModel.kt +++ b/feature/loading/src/main/java/com/vproject/texttoimage/feature/loading/LoadingViewModel.kt @@ -4,25 +4,29 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vproject.texttoimage.core.domain.GenerateImageUseCase +import com.vproject.texttoimage.core.model.data.PromptStatus import com.vproject.texttoimage.feature.loading.navigation.LoadingArgs import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import javax.inject.Inject @HiltViewModel -class LoadingViewModel @Inject constructor( +internal class LoadingViewModel @Inject constructor( savedStateHandle: SavedStateHandle, generateImageUseCase: GenerateImageUseCase, ) : ViewModel() { private val loadingArgs = LoadingArgs(savedStateHandle) val loadingUiState: StateFlow = - generateImageUseCase(loadingArgs.prompt, loadingArgs.styleId).map { imageUrl -> - LoadingUiState.Generated(imageUrl, loadingArgs.prompt, loadingArgs.styleId) + generateImageUseCase(loadingArgs.prompt, loadingArgs.styleId).map { promptData -> + if (promptData.imageUrl != null && promptData.status == PromptStatus.Success) { + LoadingUiState.Generated(promptData.imageUrl!!, loadingArgs.prompt, loadingArgs.styleId) + } else { + LoadingUiState.Error + } }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), diff --git a/feature/loading/src/main/java/com/vproject/texttoimage/feature/loading/navigation/LoadingNavigation.kt b/feature/loading/src/main/java/com/vproject/texttoimage/feature/loading/navigation/LoadingNavigation.kt index 26bcfda..cdfa0bf 100644 --- a/feature/loading/src/main/java/com/vproject/texttoimage/feature/loading/navigation/LoadingNavigation.kt +++ b/feature/loading/src/main/java/com/vproject/texttoimage/feature/loading/navigation/LoadingNavigation.kt @@ -39,13 +39,10 @@ internal class LoadingArgs(val prompt: String, val styleId: String) { fun NavController.navigateToLoading(prompt: String, styleId: String) { val encodedPrompt = URLEncoder.encode(prompt, URL_CHARACTER_ENCODING) - this.navigate("$loadingRoute/$encodedPrompt/$styleId") { - popUpTo(graph.findStartDestination().id) - launchSingleTop = true - } + this.navigate("$loadingRoute/$encodedPrompt/$styleId") } -fun NavGraphBuilder.loadingScreen(onImageGenerated: (url: String, prompt: String, styleId: String) -> Unit) { +fun NavGraphBuilder.loadingScreen(onImageGenerated: (url: String, prompt: String, styleId: String) -> Unit, onError: (message: String) -> Unit) { composable( route = "$loadingRoute/{$promptArg}/{$styleIdArg}", arguments = listOf( @@ -53,6 +50,6 @@ fun NavGraphBuilder.loadingScreen(onImageGenerated: (url: String, prompt: String navArgument(styleIdArg) { type = NavType.StringType }, ) ) { - LoadingRoute(onImageGenerated = onImageGenerated) + LoadingRoute(onImageGenerated = onImageGenerated, onError = onError) } } \ No newline at end of file diff --git a/feature/loading/src/main/res/raw/loading.json b/feature/loading/src/main/res/raw/loading.json index 864364e..439a65b 100644 --- a/feature/loading/src/main/res/raw/loading.json +++ b/feature/loading/src/main/res/raw/loading.json @@ -1 +1 @@ -{"v":"5.5.3","fr":60,"ip":0,"op":151,"w":1080,"h":1080,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Love Outlines 3","parent":2,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":126,"s":[0]},{"t":133,"s":[100]}]},"a":{"a":0,"k":[540,540,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,-62.094],[47.371,-47.37],[0,0],[0,0],[0,0],[0,0],[-94.742,94.742],[-62.095,0],[-47.371,-47.371],[0,0],[0,0],[-94.741,-94.741]],"o":[[0,62.061],[0,0],[0,0],[0,0],[0,0],[-94.742,-94.741],[47.37,-47.371],[62.06,0],[0,0],[0,0],[94.742,-94.741],[47.371,47.371]],"v":[[426.759,-124.543],[355.702,46.982],[354.927,47.758],[11.843,390.842],[-331.241,47.758],[-332.016,46.982],[-332.016,-296.102],[-160.457,-367.158],[11.068,-296.102],[11.843,-295.327],[12.618,-296.102],[355.702,-296.102]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.365000017952,0.513999968884,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[528.157,528.158]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":126,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"Null 1","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":0,"k":[540,540,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":113,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":126.514,"s":[116,116,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":133,"s":[87,87,100]},{"t":138,"s":[100,100,100]}]}},"ao":0,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Love Outlines 2","parent":2,"td":1,"sr":1,"ks":{"a":{"a":0,"k":[540,540,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,-62.094],[47.371,-47.37],[0,0],[0,0],[0,0],[0,0],[-94.742,94.742],[-62.095,0],[-47.371,-47.371],[0,0],[0,0],[-94.741,-94.741]],"o":[[0,62.061],[0,0],[0,0],[0,0],[0,0],[-94.742,-94.741],[47.37,-47.371],[62.06,0],[0,0],[0,0],[94.742,-94.741],[47.371,47.371]],"v":[[426.759,-124.543],[355.702,46.982],[354.927,47.758],[11.843,390.842],[-331.241,47.758],[-332.016,46.982],[-332.016,-296.102],[-160.457,-367.158],[11.068,-296.102],[11.843,-295.327],[12.618,-296.102],[355.702,-296.102]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.365000017952,0.513999968884,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[528.157,528.158]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 1","parent":2,"tt":1,"sr":1,"ks":{},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[104,-68],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[-104,68],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-488,-124],[-120,-460],[-440,-8],[-24,-412],[-404,80],[132,-444],[-368,176],[196,-452],[-328,232],[284,-456],[-252,256],[368,-396],[-192,284],[428,-368],[-136,320],[444,-268],[-92,352],[444,-160],[-40,400]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":1,"k":[{"i":{"x":[0.221],"y":[1]},"o":{"x":[0.564],"y":[0]},"t":0,"s":[0]},{"t":121,"s":[100]}]},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.364705882353,0.513725490196,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":48},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Love Outlines","parent":2,"sr":1,"ks":{"a":{"a":0,"k":[540,540,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,-62.094],[47.371,-47.37],[0,0],[0,0],[0,0],[0,0],[-94.742,94.742],[-62.095,0],[-47.371,-47.371],[0,0],[0,0],[-94.741,-94.741]],"o":[[0,62.061],[0,0],[0,0],[0,0],[0,0],[-94.742,-94.741],[47.37,-47.371],[62.06,0],[0,0],[0,0],[94.742,-94.741],[47.371,47.371]],"v":[[426.759,-124.543],[355.702,46.982],[354.927,47.758],[11.843,390.842],[-331.241,47.758],[-332.016,46.982],[-332.016,-296.102],[-160.457,-367.158],[11.068,-296.102],[11.843,-295.327],[12.618,-296.102],[355.702,-296.102]],"c":true}},"nm":"Path 1","hd":false},{"ty":"tr","p":{"a":0,"k":[528.157,528.158]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"st","c":{"a":0,"k":[1,0.364705882353,0.513725490196,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":48},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false}],"ip":0,"op":300,"st":0,"bm":0}],"markers":[]} \ No newline at end of file +{"v":"4.8.0","meta":{"g":"LottieFiles AE 1.0.0","a":"","k":"","d":"","tc":"#FFFFFF"},"fr":25,"ip":0,"op":41,"w":300,"h":225,"nm":"all","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"shape","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,112.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-55,0],[-55,0],[55,0],[55,0]],"o":[[55,0],[55,0],[-55,0],[-55,0]],"v":[[-53,-40],[57,40],[57,-40],[-53,40]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gs","o":{"a":0,"k":100,"ix":9},"w":{"a":0,"k":20,"ix":10},"g":{"p":3,"k":{"a":0,"k":[0,1,0.498,1,0.492,0.5,0.533,1,1,0,0.569,1],"ix":8}},"s":{"a":0,"k":[-93,-39.5],"ix":4},"e":{"a":0,"k":[110,57],"ix":5},"t":1,"lc":2,"lj":2,"bm":0,"nm":"Gradient Stroke 1","mn":"ADBE Vector Graphic - G-Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":10,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":40,"s":[360]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":52,"st":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"blur","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,114,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ef":[{"ty":29,"nm":"Gaussian Blur","np":5,"mn":"ADBE Gaussian Blur 2","ix":1,"en":1,"ef":[{"ty":0,"nm":"Blurriness","mn":"ADBE Gaussian Blur 2-0001","ix":1,"v":{"a":0,"k":10,"ix":1}},{"ty":7,"nm":"Blur Dimensions","mn":"ADBE Gaussian Blur 2-0002","ix":2,"v":{"a":0,"k":1,"ix":2}},{"ty":7,"nm":"Repeat Edge Pixels","mn":"ADBE Gaussian Blur 2-0003","ix":3,"v":{"a":0,"k":0,"ix":3}}]}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-55,0],[-55,0],[55,0],[55,0]],"o":[[55,0],[55,0],[-55,0],[-55,0]],"v":[[-53,-40],[57,40],[57,-40],[-53,40]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gs","o":{"a":0,"k":100,"ix":9},"w":{"a":0,"k":20,"ix":10},"g":{"p":3,"k":{"a":0,"k":[0,1,0.498,1,0.492,0.5,0.533,1,1,0,0.569,1],"ix":8}},"s":{"a":0,"k":[-93,-39.5],"ix":4},"e":{"a":0,"k":[110,57],"ix":5},"t":1,"lc":2,"lj":2,"bm":0,"nm":"Gradient Stroke 1","mn":"ADBE Vector Graphic - G-Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":10,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":40,"s":[360]}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":52,"st":1,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/feature/result/src/main/AndroidManifest.xml b/feature/result/src/main/AndroidManifest.xml index 1d26c87..725f702 100644 --- a/feature/result/src/main/AndroidManifest.xml +++ b/feature/result/src/main/AndroidManifest.xml @@ -1,2 +1,5 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/feature/result/src/main/java/com/vproject/texttoimage/feature/result/ResultScreen.kt b/feature/result/src/main/java/com/vproject/texttoimage/feature/result/ResultScreen.kt index 1c29358..70b5ff9 100644 --- a/feature/result/src/main/java/com/vproject/texttoimage/feature/result/ResultScreen.kt +++ b/feature/result/src/main/java/com/vproject/texttoimage/feature/result/ResultScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -37,11 +38,14 @@ import com.vproject.texttoimage.core.designsystem.icon.TextToImageIcons @Composable internal fun ResultRoute( - modifier: Modifier = Modifier, viewModel: ResultViewModel = hiltViewModel() + modifier: Modifier = Modifier, viewModel: ResultViewModel = hiltViewModel(), + onBackClick: () -> Unit ) { val resultUiState by viewModel.resultUiState.collectAsStateWithLifecycle() ResultScreen( - resultUiState = resultUiState, modifier = modifier.fillMaxSize() + modifier.fillMaxSize(), resultUiState, + onBackClick = onBackClick, + onDownloadClick = viewModel::downloadImageFromUrl ) } @@ -49,9 +53,11 @@ internal fun ResultRoute( internal fun ResultScreen( modifier: Modifier = Modifier, resultUiState: ResultUiState, + onBackClick: () -> Unit = {}, + onDownloadClick: (imageUrl: String) -> Unit = {} ) { Column(modifier = modifier) { - ResultTopAppBar(onBackClick = {}) + ResultTopAppBar(onBackClick = onBackClick) if (resultUiState is ResultUiState.ShowResult) { Column( Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 80.dp) @@ -61,7 +67,9 @@ internal fun ResultScreen( ResultStyleRow(text = resultUiState.style) ResultPromptRow(content = resultUiState.prompt) } - ResultButtonRow() + ResultButtonRow(onDownloadClick = { + onDownloadClick(resultUiState.url) + }) } } } @@ -80,12 +88,13 @@ private fun ResultTopAppBar(modifier: Modifier = Modifier, onBackClick: () -> Un @Composable private fun ResultImage(imageUrl: String) { - val imageModifier = Modifier - .heightIn(min = 180.dp) - .fillMaxWidth() - .clip(shape = MaterialTheme.shapes.medium) DynamicAsyncImage( - imageUrl = imageUrl, contentDescription = null, modifier = imageModifier + modifier = Modifier + .height(450.dp) + .fillMaxWidth() + .clip(shape = MaterialTheme.shapes.medium), + imageUrl = imageUrl, + contentDescription = null ) } @@ -158,7 +167,10 @@ private fun ResultPromptRow(modifier: Modifier = Modifier, content: String) { } @Composable -private fun ResultButtonRow(modifier: Modifier = Modifier) { +private fun ResultButtonRow( + modifier: Modifier = Modifier, + onDownloadClick: () -> Unit +) { Row( modifier = modifier .fillMaxWidth() @@ -167,6 +179,7 @@ private fun ResultButtonRow(modifier: Modifier = Modifier) { ) { TextToImageFilledButton( modifier = Modifier.weight(1f), + enabled = false, text = { Text(text = "Share") }, onClick = { /*TODO*/ }, leadingIcon = { Icon(imageVector = Icons.Default.Share, contentDescription = null) @@ -175,7 +188,9 @@ private fun ResultButtonRow(modifier: Modifier = Modifier) { Spacer(modifier = Modifier.width(10.dp)) TextToImageFilledButton( modifier = Modifier.weight(1f), - text = { Text(text = "Save") }, onClick = { /*TODO*/ }, + text = { Text(text = "Download") }, onClick = { + onDownloadClick() + }, leadingIcon = { Icon(imageVector = Icons.Default.Download, contentDescription = null) }, diff --git a/feature/result/src/main/java/com/vproject/texttoimage/feature/result/ResultUiState.kt b/feature/result/src/main/java/com/vproject/texttoimage/feature/result/ResultUiState.kt index 24a0745..535a74c 100644 --- a/feature/result/src/main/java/com/vproject/texttoimage/feature/result/ResultUiState.kt +++ b/feature/result/src/main/java/com/vproject/texttoimage/feature/result/ResultUiState.kt @@ -1,6 +1,6 @@ package com.vproject.texttoimage.feature.result -sealed interface ResultUiState { +internal sealed interface ResultUiState { object Empty: ResultUiState data class ShowResult(val url: String, val prompt: String, val style: String): ResultUiState } \ No newline at end of file diff --git a/feature/result/src/main/java/com/vproject/texttoimage/feature/result/ResultViewModel.kt b/feature/result/src/main/java/com/vproject/texttoimage/feature/result/ResultViewModel.kt index a0ae286..8cf9af1 100644 --- a/feature/result/src/main/java/com/vproject/texttoimage/feature/result/ResultViewModel.kt +++ b/feature/result/src/main/java/com/vproject/texttoimage/feature/result/ResultViewModel.kt @@ -1,20 +1,36 @@ package com.vproject.texttoimage.feature.result +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.media.MediaScannerConnection +import android.os.Environment import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import coil.ImageLoader +import coil.request.ImageRequest +import coil.request.SuccessResult import com.vproject.texttoimage.core.domain.GetFavorableStyleUseCase import com.vproject.texttoimage.feature.result.navigation.ResultArgs import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import java.io.File +import java.io.FileOutputStream +import java.io.OutputStream import javax.inject.Inject +import kotlin.random.Random + @HiltViewModel -class ResultViewModel @Inject constructor( +internal class ResultViewModel @Inject constructor( + @ApplicationContext val appContext: Context, savedStateHandle: SavedStateHandle, getFavorableStyleUseCase: GetFavorableStyleUseCase ) : ViewModel() { @@ -32,4 +48,54 @@ class ResultViewModel @Inject constructor( started = SharingStarted.WhileSubscribed(5_000), initialValue = ResultUiState.Empty ) + + + + private fun persistImage(file: File, bitmap: Bitmap) { + val os: OutputStream + try { + os = FileOutputStream(file) + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os) + os.flush() + os.close() + + MediaScannerConnection.scanFile( + appContext, arrayOf(file.toString()), + null, null + ) + + + } catch (e: Exception) { + e.printStackTrace() + } + } + + + fun downloadImageFromUrl(url: String) { + viewModelScope.launch(Dispatchers.IO) { + + val loader = ImageLoader(appContext) + val request = ImageRequest.Builder(appContext) + .data(url) + .allowHardware(false) // Disable hardware bitmaps. + .build() + + val result = (loader.execute(request) as SuccessResult).drawable + + val bitmap = (result as BitmapDrawable).bitmap + + val path = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS + "/TextToImage") //Creates app specific folder + + if (!path.exists()) + path.mkdirs() + + val imageFile = File(path, "texttoimage-${(0..1000).random()}.jpg") + if (imageFile.exists()) { + //File Name Already Exist Do Whatever + } else { + persistImage(imageFile, bitmap) + } + } + } } \ No newline at end of file diff --git a/feature/result/src/main/java/com/vproject/texttoimage/feature/result/navigation/ResultNavigation.kt b/feature/result/src/main/java/com/vproject/texttoimage/feature/result/navigation/ResultNavigation.kt index b529d46..bfedff0 100644 --- a/feature/result/src/main/java/com/vproject/texttoimage/feature/result/navigation/ResultNavigation.kt +++ b/feature/result/src/main/java/com/vproject/texttoimage/feature/result/navigation/ResultNavigation.kt @@ -27,9 +27,20 @@ const val resultRoute = "result_route" internal class ResultArgs(val imageUrl: String, val prompt: String, val styleId: String) { constructor(savedStateHandle: SavedStateHandle) : - this(URLDecoder.decode(checkNotNull(savedStateHandle[imageUrlArg]), URL_CHARACTER_ENCODING), - URLDecoder.decode(checkNotNull(savedStateHandle[promptArg]), URL_CHARACTER_ENCODING), - URLDecoder.decode(checkNotNull(savedStateHandle[styleIdArg]), URL_CHARACTER_ENCODING)) + this( + URLDecoder.decode( + checkNotNull(savedStateHandle[imageUrlArg]), + URL_CHARACTER_ENCODING + ), + URLDecoder.decode( + checkNotNull(savedStateHandle[promptArg]), + URL_CHARACTER_ENCODING + ), + URLDecoder.decode( + checkNotNull(savedStateHandle[styleIdArg]), + URL_CHARACTER_ENCODING + ) + ) } fun NavController.navigateToResult(imageUrl: String, prompt: String, styleId: String) { @@ -40,15 +51,15 @@ fun NavController.navigateToResult(imageUrl: String, prompt: String, styleId: St } } -fun NavGraphBuilder.resultScreen() { +fun NavGraphBuilder.resultScreen(onBackClick: () -> Unit) { composable( route = "$resultRoute/{$imageUrlArg}/{$promptArg}/{$styleIdArg}", arguments = listOf( navArgument(imageUrlArg) { type = NavType.StringType }, navArgument(promptArg) { type = NavType.StringType }, navArgument(styleIdArg) { type = NavType.StringType }, - ) + ) ) { - ResultRoute() + ResultRoute(onBackClick = onBackClick) } } \ No newline at end of file diff --git a/feature/settings/src/main/java/com/vproject/texttoimage/feature/settings/GeneralSettingType.kt b/feature/settings/src/main/java/com/vproject/texttoimage/feature/settings/GeneralSettingType.kt index 2cee5eb..7328948 100644 --- a/feature/settings/src/main/java/com/vproject/texttoimage/feature/settings/GeneralSettingType.kt +++ b/feature/settings/src/main/java/com/vproject/texttoimage/feature/settings/GeneralSettingType.kt @@ -3,7 +3,7 @@ package com.vproject.texttoimage.feature.settings import androidx.compose.ui.graphics.vector.ImageVector import com.vproject.texttoimage.core.designsystem.icon.TextToImageIcons -enum class GeneralSettingType( +internal enum class GeneralSettingType( val leadingIcon: ImageVector, val titleTextId: Int, ) { diff --git a/feature/settings/src/main/java/com/vproject/texttoimage/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/vproject/texttoimage/feature/settings/SettingsScreen.kt index 060a63d..47d4c24 100644 --- a/feature/settings/src/main/java/com/vproject/texttoimage/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/vproject/texttoimage/feature/settings/SettingsScreen.kt @@ -125,7 +125,7 @@ private fun AdvancedPromptOptionSectionCard( ) { Card( shape = RoundedCornerShape(16.dp), - border = BorderStroke(2.dp, color = MaterialTheme.colorScheme.onSecondary), + border = BorderStroke(2.dp, color = MaterialTheme.colorScheme.primary), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), modifier = modifier.fillMaxWidth() ) { @@ -181,7 +181,7 @@ private fun AdvancedPromptOptionItem( Text( advancedExplanation, style = TextStyle( - color = MaterialTheme.colorScheme.background, + color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Normal, fontSize = 14.sp ), @@ -197,7 +197,7 @@ private fun GeneralSectionCard( ) { Card( shape = RoundedCornerShape(16.dp), - border = BorderStroke(2.dp, color = MaterialTheme.colorScheme.onSecondary), + border = BorderStroke(2.dp, color = MaterialTheme.colorScheme.primary), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), modifier = modifier.fillMaxWidth() ) { diff --git a/feature/settings/src/main/java/com/vproject/texttoimage/feature/settings/SettingsUiState.kt b/feature/settings/src/main/java/com/vproject/texttoimage/feature/settings/SettingsUiState.kt index 34f7da9..8902c4f 100644 --- a/feature/settings/src/main/java/com/vproject/texttoimage/feature/settings/SettingsUiState.kt +++ b/feature/settings/src/main/java/com/vproject/texttoimage/feature/settings/SettingsUiState.kt @@ -2,7 +2,7 @@ package com.vproject.texttoimage.feature.settings import com.vproject.texttoimage.core.model.data.UserEditableSettings -sealed interface SettingsUiState { +internal sealed interface SettingsUiState { object Loading: SettingsUiState data class Success(val settings: UserEditableSettings) : SettingsUiState } \ No newline at end of file diff --git a/feature/settings/src/main/java/com/vproject/texttoimage/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/vproject/texttoimage/feature/settings/SettingsViewModel.kt index 85c4fb2..3c78d98 100644 --- a/feature/settings/src/main/java/com/vproject/texttoimage/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/vproject/texttoimage/feature/settings/SettingsViewModel.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class SettingsViewModel @Inject constructor( +internal class SettingsViewModel @Inject constructor( getUserEditableSettingsUseCase: GetUserEditableSettingsUseCase, private val setPromptCfgScaleValueUseCase: SetPromptCfgScaleValueUseCase, private val setPromptStepValueUseCase: SetPromptStepValueUseCase, diff --git a/feature/settings/src/main/res/values/strings.xml b/feature/settings/src/main/res/values/strings.xml index 0e8ae4e..8cb2d2d 100644 --- a/feature/settings/src/main/res/values/strings.xml +++ b/feature/settings/src/main/res/values/strings.xml @@ -1,15 +1,15 @@ Settings - Advanced Prompt Options - CFG Scale + Advanced prompt options + CFG scale Higher values will keep your artwork more in line with your prompt Steps Running more steps means better image quality but generating may take more time General - Display Language - Dark Mode - Privacy Policy - Term of Service + Display language + Dark mode + Privacy policy + Term of service About Text To Image \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 688fb9d..b06962d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,4 +22,4 @@ kotlin.code.style=official # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -STABLE_DIFFUSION_API_KEY=LY1dqjy5kCBSQBN31xMFvpFsTJXN3vsk4wasWg6TNsy4HQZxlCYb08gXBKjy \ No newline at end of file +STABLE_DIFFUSION_API_KEY= \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 9a3b948..18ffd80 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,7 +16,7 @@ dependencyResolutionManagement { rootProject.name = "TextToImage" include(":app") include(":feature:generate") -include(":feature:explore") +include(":feature:gallery") include(":feature:settings") include(":feature:loading") include(":feature:result")