Sharing animation

This commit is contained in:
2021-09-08 00:48:20 +02:00
parent 3397efe47f
commit 09ce7b6fb8
15 changed files with 469 additions and 99 deletions
+4 -7
View File
@@ -1,9 +1,9 @@
<h1 align="center">Lottiefiles App</h1></br> <h1 align="center">Lottiefiles App</h1><br>
<p align="center"> <p align="center">
Lottiefiles App is an Android application built using jetpack compose and Hilt, based on modern Android technology stacks and the MVI architecture. Lottiefiles App is an Android application built using jetpack compose and Hilt, based on modern Android technology stacks and the MVI architecture.
The application is a clone of the [Lottiefiles](https://lottiefiles.com/mobile) mobile application and allows the user to retrieve and save animations, animators and articles locally from an API rest The application is a clone of the [Lottiefiles](https://lottiefiles.com/mobile) mobile application and allows the user to retrieve and save animations, animators and articles locally from an API rest
</p> </p>
</br> <br>
<p align="center"> <p align="center">
<a href="https://opensource.org/licenses/Apache-2.0"><img alt="License" src="https://img.shields.io/badge/License-Apache%202.0-blue.svg"/></a> <a href="https://opensource.org/licenses/Apache-2.0"><img alt="License" src="https://img.shields.io/badge/License-Apache%202.0-blue.svg"/></a>
@@ -46,12 +46,9 @@ Go to the [Releases](https://github.com/eric-ampire/lottiefiles-app/releases) to
* The API was not complete and the stories and animators returned by the API did not have IDs, I had to find a strategy to assign IDs to data that did not have them to avoid duplication in the database. * The API was not complete and the stories and animators returned by the API did not have IDs, I had to find a strategy to assign IDs to data that did not have them to avoid duplication in the database.
* Animations didn't come with any information about whether an animation was popular, recent or featured, so I had to add an extra field to make the queries properly. * Animations didn't come with any information about whether an animation was popular, recent or featured, so I had to add an extra field to make the queries properly.
## Find this repository useful? :heart:
Support it by joining __[stargazers](https://github.com/eric-ampire/lottiefiles-app/stargazers)__ for this repository. :star: <br>
And __[follow](https://github.com/eric-ampire)__ me for my next creations! 🤩
# License # License
```xml
```
Designed and developed by 2020 ericampire (Eric Ampire) Designed and developed by 2020 ericampire (Eric Ampire)
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
+1 -1
View File
@@ -130,5 +130,5 @@ dependencies {
testImplementation(Libs.mockk_core) testImplementation(Libs.mockk_core)
androidTestImplementation(Libs.mockk_android) androidTestImplementation(Libs.mockk_android)
debugImplementation(Libs.code_scanner) implementation(Libs.code_scanner)
} }
+13
View File
@@ -39,6 +39,16 @@
</intent-filter> </intent-filter>
</activity> </activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<provider <provider
android:name="androidx.startup.InitializationProvider" android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup" android:authorities="${applicationId}.androidx-startup"
@@ -50,6 +60,9 @@
<meta-data <meta-data
android:name="com.ericampire.android.androidstudycase.app.initializer.MavericksInitializer" android:name="com.ericampire.android.androidstudycase.app.initializer.MavericksInitializer"
android:value="androidx.startup" /> android:value="androidx.startup" />
<meta-data
android:name="com.ericampire.android.androidstudycase.app.initializer.DownloaderInitializer"
android:value="androidx.startup" />
</provider> </provider>
</application> </application>
@@ -0,0 +1,19 @@
package com.ericampire.android.androidstudycase.app.initializer
import android.content.Context
import androidx.startup.Initializer
import com.downloader.PRDownloader
import com.downloader.PRDownloaderConfig
class DownloaderInitializer : Initializer<Unit> {
override fun create(context: Context) {
val config = PRDownloaderConfig.newBuilder()
.setDatabaseEnabled(true)
.build()
PRDownloader.initialize(context, config)
}
override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf()
}
}
@@ -32,6 +32,8 @@ import com.ericampire.android.androidstudycase.presentation.screen.explore.busin
import com.ericampire.android.androidstudycase.presentation.screen.explore.business.ExploreViewModel import com.ericampire.android.androidstudycase.presentation.screen.explore.business.ExploreViewModel
import com.ericampire.android.androidstudycase.presentation.screen.explore.business.ExploreViewState import com.ericampire.android.androidstudycase.presentation.screen.explore.business.ExploreViewState
import com.ericampire.android.androidstudycase.presentation.theme.AppColor import com.ericampire.android.androidstudycase.presentation.theme.AppColor
import com.ericampire.android.androidstudycase.util.extension.copyTextToClipboard
import com.ericampire.android.androidstudycase.util.extension.downloadAndShare
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.pagerTabIndicatorOffset import com.google.accompanist.pager.pagerTabIndicatorOffset
@@ -55,6 +57,11 @@ fun ExploreScreen(
val tabItems = stringArrayResource(id = R.array.explore_item) val tabItems = stringArrayResource(id = R.array.explore_item)
val pagerState = rememberPagerState(pageCount = tabItems.size) val pagerState = rememberPagerState(pageCount = tabItems.size)
val bottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
var selectedAnimation by remember { mutableStateOf<Lottiefile?>(null) }
var fileUrl by remember { mutableStateOf<String?>(null) }
LaunchedEffect(viewModel) { LaunchedEffect(viewModel) {
viewModel.submitAction(ExploreAction.FindRecentFile) viewModel.submitAction(ExploreAction.FindRecentFile)
} }
@@ -69,6 +76,44 @@ fun ExploreScreen(
} }
} }
LaunchedEffect(fileUrl) {
if (fileUrl != null) {
context.downloadAndShare(
url = fileUrl!!,
onError = {
Timber.e(it)
},
onSuccess = {
selectedAnimation = null
fileUrl = null
coroutineScope.launch {
bottomSheetState.hide()
}
}
)
}
}
ModalBottomSheetLayout(
sheetShape = RoundedCornerShape(topEnd = 24.dp, topStart = 24.dp),
sheetState = bottomSheetState,
sheetContent = {
ShareBottomSheet(
lottiefile = selectedAnimation,
isLoading = fileUrl != null,
onCopyLink = {
context.copyTextToClipboard(it)
coroutineScope.launch {
bottomSheetState.hide()
}
},
onShareGifFile = { fileUrl = it },
onShareJsonFile = { fileUrl = it },
onShareVideoFile = { fileUrl = it }
)
},
content = {
Scaffold( Scaffold(
topBar = { topBar = {
Column( Column(
@@ -148,7 +193,15 @@ fun ExploreScreen(
) )
} else { } else {
HorizontalPager(state = pagerState) { HorizontalPager(state = pagerState) {
ExploreContent(files = animations) ExploreContent(
files = animations,
onShareClick = {
selectedAnimation = it
coroutineScope.launch {
bottomSheetState.animateTo(ModalBottomSheetValue.Expanded)
}
}
)
} }
} }
} }
@@ -161,12 +214,15 @@ fun ExploreScreen(
} }
} }
) )
}
)
} }
@ExperimentalMaterialApi @ExperimentalMaterialApi
@Composable @Composable
private fun ExploreContent( private fun ExploreContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onShareClick: (Lottiefile) -> Unit,
files: List<Lottiefile> files: List<Lottiefile>
) { ) {
LazyColumn( LazyColumn(
@@ -184,7 +240,8 @@ private fun ExploreContent(
items(items = files, key = { it.toString() }) { lottieFile -> items(items = files, key = { it.toString() }) { lottieFile ->
LottieFileItemView( LottieFileItemView(
lottiefile = lottieFile, lottiefile = lottieFile,
onClick = {} onClick = {},
onShareClick = onShareClick,
) )
Divider( Divider(
@@ -31,7 +31,8 @@ import com.ericampire.android.androidstudycase.util.LottieFileProvider
fun LottieFileItemView( fun LottieFileItemView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
lottiefile: Lottiefile, lottiefile: Lottiefile,
onClick: (Lottiefile) -> Unit onClick: (Lottiefile) -> Unit,
onShareClick: (Lottiefile) -> Unit,
) { ) {
Card( Card(
modifier = modifier, modifier = modifier,
@@ -89,7 +90,9 @@ fun LottieFileItemView(
onLikeClick = { /*TODO*/ }, onLikeClick = { /*TODO*/ },
onCommentClick = { /*TODO*/ }, onCommentClick = { /*TODO*/ },
onAddCollectionClick = { /*TODO*/ }, onAddCollectionClick = { /*TODO*/ },
onShareClick = { } onShareClick = {
onShareClick(lottiefile)
}
) )
} }
) )
@@ -203,7 +206,8 @@ fun LottiefileItemViewPreview(@PreviewParameter(LottieFileProvider::class) data:
content = { content = {
LottieFileItemView( LottieFileItemView(
lottiefile = data, lottiefile = data,
onClick = {} onClick = {},
onShareClick = {},
) )
} }
) )
@@ -0,0 +1,201 @@
package com.ericampire.android.androidstudycase.presentation.screen.explore.ui
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.InsertDriveFile
import androidx.compose.material.icons.rounded.Link
import androidx.compose.runtime.Composable
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.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import com.ericampire.android.androidstudycase.R
import com.ericampire.android.androidstudycase.domain.entity.Lottiefile
import com.ericampire.android.androidstudycase.presentation.theme.AndroidStudyCaseTheme
import com.ericampire.android.androidstudycase.presentation.theme.AppColor
import com.ericampire.android.androidstudycase.util.LottieFileProvider
@Composable
fun ShareBottomSheet(
modifier: Modifier = Modifier,
isLoading: Boolean = false,
lottiefile: Lottiefile?,
onCopyLink: (String) -> Unit,
onShareJsonFile: (String?) -> Unit,
onShareGifFile: (String?) -> Unit,
onShareVideoFile: (String?) -> Unit,
) {
val progressBarAlpha = if (isLoading) 1f else 0f
Column(
modifier = modifier
.background(AppColor.Black001),
content = {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.alpha(progressBarAlpha)
)
Column(
modifier = modifier
.padding(16.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
content = {
Text(
text = stringResource(id = R.string.txt_share_animation),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.h4.copy(
color = MaterialTheme.colors.onSurface
),
)
Column(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.background(AppColor.Arsenic)
.clickable { onCopyLink(lottiefile?.lottieUrl ?: "") }
.padding(18.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
horizontalAlignment = Alignment.CenterHorizontally,
content = {
Icon(
modifier = Modifier.size(48.dp),
tint = AppColor.SlateGray,
imageVector = Icons.Rounded.Link,
contentDescription = null
)
Text(
textAlign = TextAlign.Center,
text = stringResource(id = R.string.txt_share_animation),
style = MaterialTheme.typography.body1.copy(
color = MaterialTheme.colors.onSurface
),
)
}
)
Column(
modifier = Modifier
.clip(MaterialTheme.shapes.medium)
.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
content = {
ShareItem(
modifier = Modifier.clickable {
onShareJsonFile(lottiefile?.lottieUrl)
},
title = "Lottie JSON",
fileExtension = ".json",
icon = Icons.Rounded.InsertDriveFile
)
Divider(modifier = Modifier
.fillMaxWidth()
.background(Color.DarkGray))
ShareItem(
modifier = Modifier.clickable {
onShareGifFile(lottiefile?.gifUrl)
},
title = "Animated GIF",
fileExtension = ".gif",
icon = Icons.Rounded.InsertDriveFile
)
Divider(modifier = Modifier
.fillMaxWidth()
.background(Color.DarkGray))
ShareItem(
modifier = Modifier.clickable {
onShareVideoFile(lottiefile?.videoUrl)
},
title = "Video MP4",
fileExtension = ".mp4",
icon = Icons.Rounded.InsertDriveFile
)
}
)
}
)
}
)
}
@Composable
private fun ShareItem(
modifier: Modifier = Modifier,
title: String,
fileExtension: String,
icon: ImageVector
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.background(AppColor.Arsenic)
.padding(24.dp)
.fillMaxWidth(),
content = {
Row(
horizontalArrangement = Arrangement.spacedBy(24.dp),
verticalAlignment = Alignment.CenterVertically,
content = {
Icon(
imageVector = icon,
tint = AppColor.SlateGray,
contentDescription = null
)
Text(
text = title,
style = MaterialTheme.typography.h4.copy(
color = MaterialTheme.colors.onSurface
),
)
}
)
Text(
text = fileExtension,
style = MaterialTheme.typography.body1.copy(
color = Color.Gray
),
)
}
)
}
@ExperimentalAnimationApi
@ExperimentalMaterialApi
@Preview()
@Composable
fun ShareBottomSheetPreview(@PreviewParameter(LottieFileProvider::class) data: Lottiefile) {
AndroidStudyCaseTheme(darkTheme = true) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
content = {
ShareBottomSheet(
lottiefile = data,
isLoading = true,
onCopyLink = {},
onShareGifFile = {},
onShareJsonFile = {},
onShareVideoFile = {}
)
}
)
}
}
@@ -1,6 +1,5 @@
package com.ericampire.android.androidstudycase.presentation.screen.home.ui package com.ericampire.android.androidstudycase.presentation.screen.home.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -12,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -32,6 +32,8 @@ fun LoginBottomSheet(
onLoginClick: () -> Unit, onLoginClick: () -> Unit,
) { ) {
val progressBarAlpha = if (isLoading) 1f else 0f
val composition by rememberLottieComposition( val composition by rememberLottieComposition(
spec = LottieCompositionSpec.RawRes(R.raw.people_communicating) spec = LottieCompositionSpec.RawRes(R.raw.people_communicating)
) )
@@ -45,9 +47,11 @@ fun LoginBottomSheet(
.clip(MaterialTheme.shapes.medium) .clip(MaterialTheme.shapes.medium)
.background(AppColor.Black001), .background(AppColor.Black001),
content = { content = {
AnimatedVisibility(isLoading) { LinearProgressIndicator(
LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) modifier = Modifier
} .fillMaxWidth()
.alpha(progressBarAlpha)
)
Column( Column(
modifier = Modifier.padding(24.dp), modifier = Modifier.padding(24.dp),
@@ -100,7 +104,7 @@ fun LoginBottomSheet(
@ExperimentalAnimationApi @ExperimentalAnimationApi
@ExperimentalMaterialApi @ExperimentalMaterialApi
@Preview() @Preview
@Composable @Composable
fun LoginDialogViewPreview() { fun LoginDialogViewPreview() {
AndroidStudyCaseTheme(darkTheme = true) { AndroidStudyCaseTheme(darkTheme = true) {
@@ -5,6 +5,8 @@ import androidx.compose.ui.graphics.Color
object AppColor { object AppColor {
val SlateGray = Color(0xFF606E7C)
val Arsenic = Color(0xFF3C3C3C)
val Teal = Color(0xFF1C7373) val Teal = Color(0xFF1C7373)
val PaleBlue = Color(0xFFD3F6F6) val PaleBlue = Color(0xFFD3F6F6)
val PrimaryColor = Color(0xFF2BEAED) val PrimaryColor = Color(0xFF2BEAED)
+1
View File
@@ -5,4 +5,5 @@
<string name="txt_login_title">1000s of lottie animations</string> <string name="txt_login_title">1000s of lottie animations</string>
<string name="txt_login_description">from top creators waiting to be discover, save and share from the palm of your hand</string> <string name="txt_login_description">from top creators waiting to be discover, save and share from the palm of your hand</string>
<string name="txt_connect_with_facebook">Continue with Facebook</string> <string name="txt_connect_with_facebook">Continue with Facebook</string>
<string name="txt_share_animation">Share animation</string>
</resources> </resources>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external_files"
path="." />
</paths>
+1
View File
@@ -148,4 +148,5 @@ object Libs {
const val joda_time = "net.danlew:android.joda:2.10.9" const val joda_time = "net.danlew:android.joda:2.10.9"
const val code_scanner = "com.budiyev.android:code-scanner:2.1.0" const val code_scanner = "com.budiyev.android:code-scanner:2.1.0"
const val fetch = "com.mindorks.android:prdownloader:0.6.0"
} }
+1
View File
@@ -23,4 +23,5 @@
<string name="txt_permission_denied">Camera permission denied. See this FAQ with information about why we need this permission. Please, grant us access on the Settings screen.</string> <string name="txt_permission_denied">Camera permission denied. See this FAQ with information about why we need this permission. Please, grant us access on the Settings screen.</string>
<string name="txt_connect_with_google">Continue with Google</string> <string name="txt_connect_with_google">Continue with Google</string>
<string name="txt_get_started">Get started for free</string> <string name="txt_get_started">Get started for free</string>
<string name="txt_share_lottie_file">Share Lottiefile</string>
</resources> </resources>
+1
View File
@@ -51,4 +51,5 @@ dependencies {
kapt(Libs.hilt_android_compiler) kapt(Libs.hilt_android_compiler)
api(Libs.timber) api(Libs.timber)
api(Libs.fetch)
} }
@@ -0,0 +1,63 @@
package com.ericampire.android.androidstudycase.util.extension
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Environment
import android.widget.Toast
import androidx.core.app.ShareCompat
import androidx.core.content.FileProvider
import com.downloader.Error
import com.downloader.OnDownloadListener
import com.downloader.PRDownloader
import com.downloader.utils.Utils
import org.zxconnect.android.beserve.util.R
import java.io.File
fun Context.downloadAndShare(
url: String,
onSuccess: () -> Unit,
onError: (String?) -> Unit
) {
val dirPath = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString()
val fileName = url.substringAfterLast("/")
val filePath = Utils.getPath(dirPath, fileName)
val downloadRequest = PRDownloader.download(url, dirPath, fileName).build()
downloadRequest.start(object : OnDownloadListener {
override fun onDownloadComplete() {
shareFile(filePath)
onSuccess()
}
override fun onError(error: Error) {
onError(error.serverErrorMessage)
}
})
}
fun Context.copyTextToClipboard(text: String) {
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText("text", text)
clipboardManager.setPrimaryClip(clipData)
Toast.makeText(this, "Link copied to clipboard", Toast.LENGTH_LONG).show()
}
private fun Context.shareFile(filePath: String) {
val file = File(filePath)
val fileUri = FileProvider.getUriForFile(
this,
this.applicationContext.packageName.toString() + ".provider",
file
)
val shareIntent = ShareCompat.IntentBuilder(this)
.setChooserTitle(getString(R.string.txt_share_lottie_file))
.setStream(fileUri)
.setType("application/pdf")
.createChooserIntent()
startActivity(shareIntent)
}