Added sync library after saving from share menu
This commit is contained in:
@ -168,6 +168,9 @@ dependencies {
|
||||
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
|
||||
implementation(libs.work.runtime.ktx)
|
||||
implementation(libs.hilt.work)
|
||||
ksp(libs.hilt.work.compiler)
|
||||
}
|
||||
|
||||
apollo {
|
||||
|
||||
@ -55,5 +55,11 @@
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.Omnivore"/>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
tools:node="remove">
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@ -1,11 +1,23 @@
|
||||
package app.omnivore.omnivore
|
||||
|
||||
import android.app.Application
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.work.Configuration
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import io.intercom.android.sdk.Intercom
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class OmnivoreApplication: Application() {
|
||||
class OmnivoreApplication: Application(), Configuration.Provider {
|
||||
|
||||
@Inject
|
||||
lateinit var workerFactory: HiltWorkerFactory
|
||||
|
||||
override val workManagerConfiguration
|
||||
get() = Configuration.Builder()
|
||||
.setWorkerFactory(workerFactory)
|
||||
.build()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
|
||||
@ -0,0 +1,100 @@
|
||||
package app.omnivore.omnivore.feature.library
|
||||
|
||||
import android.content.Context
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import app.omnivore.omnivore.core.data.repository.LibraryRepository
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
import app.omnivore.omnivore.core.datastore.libraryLastSyncTimestamp
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.Instant
|
||||
|
||||
@HiltWorker
|
||||
class LibrarySyncWorker @AssistedInject constructor(
|
||||
@Assisted appContext: Context,
|
||||
@Assisted workerParams: WorkerParameters,
|
||||
private val libraryRepository: LibraryRepository,
|
||||
private val datastoreRepository: DatastoreRepository,
|
||||
) : CoroutineWorker(appContext, workerParams) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
performItemSync()
|
||||
loadUsingSearchAPI()
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun performItemSync(
|
||||
cursor: String? = null,
|
||||
since: String = getLastSyncTime()?.toString() ?: Instant.MIN.toString(),
|
||||
count: Int = 0,
|
||||
syncStart: String = Instant.now().toString(),
|
||||
) {
|
||||
libraryRepository.syncOfflineItemsWithServerIfNeeded()
|
||||
|
||||
val result = libraryRepository.sync(
|
||||
context = applicationContext,
|
||||
since = since,
|
||||
cursor = cursor,
|
||||
limit = 10
|
||||
)
|
||||
val totalCount = count + result.count
|
||||
|
||||
if (result.hasError) {
|
||||
result.errorString?.let { errorString ->
|
||||
println("SYNC ERROR: $errorString")
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.hasError && result.hasMoreItems && result.cursor != null) {
|
||||
performItemSync(
|
||||
cursor = result.cursor,
|
||||
since = since,
|
||||
count = totalCount,
|
||||
syncStart = syncStart
|
||||
)
|
||||
} else {
|
||||
datastoreRepository.putString(libraryLastSyncTimestamp, syncStart)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadUsingSearchAPI() {
|
||||
val result = libraryRepository.librarySearch(
|
||||
context = applicationContext,
|
||||
cursor = null,
|
||||
query = "${SavedItemFilter.INBOX.queryString} ${SavedItemSortFilter.NEWEST.queryString}"
|
||||
)
|
||||
result.savedItems.map {
|
||||
val isSavedInDB =
|
||||
libraryRepository.isSavedItemContentStoredInDB(
|
||||
applicationContext,
|
||||
it.savedItem.slug
|
||||
)
|
||||
|
||||
if (!isSavedInDB) {
|
||||
libraryRepository.fetchSavedItemContent(applicationContext, it.savedItem.slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLastSyncTime(): Instant? = runBlocking {
|
||||
datastoreRepository.getString(libraryLastSyncTimestamp)?.let {
|
||||
try {
|
||||
return@let Instant.parse(it)
|
||||
} catch (e: Exception) {
|
||||
return@let null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,6 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@ -13,17 +12,27 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment.Companion.TopCenter
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.OutOfQuotaPolicy
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.workDataOf
|
||||
import app.omnivore.omnivore.feature.library.LibrarySyncWorker
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
// Not sure why we need this class, but directly opening SaveSheetActivity
|
||||
// causes the app to crash.
|
||||
@ -32,159 +41,196 @@ class SaveSheetActivity : SaveSheetActivityBase()
|
||||
@AndroidEntryPoint
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
abstract class SaveSheetActivityBase : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val viewModel: SaveViewModel by viewModels()
|
||||
var extractedText: String?
|
||||
var extractedText: String? = null
|
||||
var saveState: SaveState by mutableStateOf(SaveState.DEFAULT)
|
||||
val workManager = WorkManager.getInstance(applicationContext)
|
||||
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_SEND -> {
|
||||
if (intent.type?.startsWith("text/plain") == true) {
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||
extractedText = it
|
||||
viewModel.saveURL(it)
|
||||
Log.d(ContentValues.TAG, "Extracted text: $extractedText")
|
||||
}
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_SEND -> {
|
||||
if (intent.type?.startsWith("text/plain") == true) {
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||
extractedText = it
|
||||
workManager.enqueueSaveWorker(it)
|
||||
Log.d(ContentValues.TAG, "Extracted text: $extractedText")
|
||||
}
|
||||
}
|
||||
if (intent.type?.startsWith("text/html") == true) {
|
||||
intent.getStringExtra(Intent.EXTRA_HTML_TEXT)?.let {
|
||||
extractedText = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Handle other intents, such as being started from the home screen
|
||||
}
|
||||
}
|
||||
if (intent.type?.startsWith("text/html") == true) {
|
||||
intent.getStringExtra(Intent.EXTRA_HTML_TEXT)?.let {
|
||||
extractedText = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Handle other intents, such as being started from the home screen
|
||||
}
|
||||
setContent {
|
||||
LaunchedEffect(extractedText) {
|
||||
extractedText?.let { url ->
|
||||
workManager.getWorkInfosByTagFlow(url).map {
|
||||
saveState = when (it.firstOrNull()?.state) {
|
||||
WorkInfo.State.RUNNING -> SaveState.SAVING
|
||||
WorkInfo.State.SUCCEEDED -> SaveState.SAVED
|
||||
WorkInfo.State.FAILED -> SaveState.ERROR
|
||||
else -> SaveState.SAVING
|
||||
}
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
val scaffoldState: ScaffoldState = rememberScaffoldState()
|
||||
|
||||
|
||||
val message = when (saveState) {
|
||||
SaveState.DEFAULT -> ""
|
||||
SaveState.SAVING -> "Saved to Omnivore"
|
||||
SaveState.ERROR -> "Error Saving Article"
|
||||
SaveState.SAVED -> "Saved to Omnivore"
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.clickable {
|
||||
Log.d("debug", "DISMISS SCAFFOLD")
|
||||
exit()
|
||||
},
|
||||
scaffoldState = scaffoldState,
|
||||
backgroundColor = Color.Transparent,
|
||||
|
||||
// TODO: In future versions we can present Label, Note, Highlight options here
|
||||
bottomBar = {
|
||||
|
||||
androidx.compose.material3.BottomAppBar(
|
||||
|
||||
modifier = Modifier
|
||||
.height(55.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(topEnd = 5.dp, topStart = 5.dp)),
|
||||
containerColor = MaterialTheme.colors.background,
|
||||
actions = {
|
||||
Spacer(modifier = Modifier.width(25.dp))
|
||||
Text(
|
||||
message,
|
||||
style = androidx.compose.material3.MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
LaunchedEffect(saveState) {
|
||||
if (saveState == SaveState.SAVED) {
|
||||
delay(1.5.seconds)
|
||||
exit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
val saveState: SaveState by viewModel.state.observeAsState(SaveState.DEFAULT)
|
||||
val scaffoldState: ScaffoldState = rememberScaffoldState()
|
||||
private fun WorkManager.enqueueSaveWorker(url: String) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
val saveWorkerRequest = OneTimeWorkRequestBuilder<SaveURLWorker>()
|
||||
.setConstraints(constraints)
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
.setInputData(workDataOf("url" to url))
|
||||
.addTag(url)
|
||||
// Can add other configs like setBackoffCriteria to retry sync if failed
|
||||
.build()
|
||||
val syncWorkerRequest = OneTimeWorkRequestBuilder<LibrarySyncWorker>()
|
||||
.setConstraints(constraints)
|
||||
.addTag(url)
|
||||
// delay to make sure the url title and content are fetched
|
||||
.setInitialDelay(10.seconds.toJavaDuration())
|
||||
.build()
|
||||
|
||||
val message = when (saveState) {
|
||||
SaveState.DEFAULT -> ""
|
||||
SaveState.SAVING -> "Saved to Omnivore"
|
||||
SaveState.ERROR -> "Error Saving Article"
|
||||
SaveState.SAVED -> "Saved to Omnivore"
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.clickable {
|
||||
Log.d("debug", "DISMISS SCAFFOLD")
|
||||
exit()
|
||||
},
|
||||
scaffoldState = scaffoldState,
|
||||
backgroundColor = Color.Transparent,
|
||||
|
||||
// TODO: In future versions we can present Label, Note, Highlight options here
|
||||
bottomBar = {
|
||||
|
||||
androidx.compose.material3.BottomAppBar(
|
||||
beginWith(saveWorkerRequest)
|
||||
.then(syncWorkerRequest)
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomSheetUI(content: @Composable () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(55.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(topEnd = 5.dp, topStart = 5.dp)),
|
||||
containerColor = MaterialTheme.colors.background,
|
||||
actions = {
|
||||
Spacer(modifier = Modifier.width(25.dp))
|
||||
Text(
|
||||
message,
|
||||
style = androidx.compose.material3.MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
) {
|
||||
.wrapContentHeight()
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp))
|
||||
.background(Color.White)
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
content()
|
||||
|
||||
}
|
||||
|
||||
LaunchedEffect(saveState) {
|
||||
if (saveState == SaveState.SAVED) {
|
||||
delay(1.5.seconds)
|
||||
exit()
|
||||
Divider(
|
||||
color = Color.Gray,
|
||||
thickness = 5.dp,
|
||||
modifier = Modifier
|
||||
.padding(top = 15.dp)
|
||||
.align(TopCenter)
|
||||
.width(80.dp)
|
||||
.clip(RoundedCornerShape(50.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomSheetUI(content: @Composable () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.wrapContentHeight()
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp))
|
||||
.background(Color.White)
|
||||
.statusBarsPadding()
|
||||
// Helper methods
|
||||
private suspend fun handleBottomSheetAtHiddenState(
|
||||
isSheetOpened: MutableState<Boolean>,
|
||||
modalBottomSheetState: ModalBottomSheetState
|
||||
) {
|
||||
content()
|
||||
|
||||
Divider(
|
||||
color = Color.Gray,
|
||||
thickness = 5.dp,
|
||||
modifier = Modifier
|
||||
.padding(top = 15.dp)
|
||||
.align(TopCenter)
|
||||
.width(80.dp)
|
||||
.clip(RoundedCornerShape(50.dp))
|
||||
)
|
||||
when {
|
||||
!isSheetOpened.value -> initializeModalLayout(isSheetOpened, modalBottomSheetState)
|
||||
else -> exit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private suspend fun handleBottomSheetAtHiddenState(
|
||||
isSheetOpened: MutableState<Boolean>,
|
||||
modalBottomSheetState: ModalBottomSheetState
|
||||
) {
|
||||
when {
|
||||
!isSheetOpened.value -> initializeModalLayout(isSheetOpened, modalBottomSheetState)
|
||||
else -> exit()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initializeModalLayout(
|
||||
isSheetOpened: MutableState<Boolean>,
|
||||
modalBottomSheetState: ModalBottomSheetState
|
||||
) {
|
||||
isSheetOpened.value = true
|
||||
modalBottomSheetState.show()
|
||||
}
|
||||
|
||||
open fun exit() = finish()
|
||||
|
||||
private fun onFinish(
|
||||
coroutineScope: CoroutineScope,
|
||||
modalBottomSheetState: ModalBottomSheetState,
|
||||
withResults: Boolean = false,
|
||||
result: Intent? = null
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
if (withResults) setResult(RESULT_OK)
|
||||
result?.let { intent = it }
|
||||
modalBottomSheetState.hide() // will trigger the LaunchedEffect
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScreenContent(
|
||||
viewModel: SaveViewModel,
|
||||
modalBottomSheetState: ModalBottomSheetState
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(300.dp)
|
||||
.background(Color.White)
|
||||
private suspend fun initializeModalLayout(
|
||||
isSheetOpened: MutableState<Boolean>,
|
||||
modalBottomSheetState: ModalBottomSheetState
|
||||
) {
|
||||
SaveContent(viewModel, modalBottomSheetState, modifier = Modifier.fillMaxSize())
|
||||
isSheetOpened.value = true
|
||||
modalBottomSheetState.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
open fun exit() = finish()
|
||||
|
||||
private fun onFinish(
|
||||
coroutineScope: CoroutineScope,
|
||||
modalBottomSheetState: ModalBottomSheetState,
|
||||
withResults: Boolean = false,
|
||||
result: Intent? = null
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
if (withResults) setResult(RESULT_OK)
|
||||
result?.let { intent = it }
|
||||
modalBottomSheetState.hide() // will trigger the LaunchedEffect
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScreenContent(
|
||||
viewModel: SaveViewModel,
|
||||
modalBottomSheetState: ModalBottomSheetState
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(300.dp)
|
||||
.background(Color.White)
|
||||
) {
|
||||
SaveContent(viewModel, modalBottomSheetState, modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,81 @@
|
||||
package app.omnivore.omnivore.feature.save
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
import app.omnivore.omnivore.core.datastore.omnivoreAuthToken
|
||||
import app.omnivore.omnivore.graphql.generated.SaveUrlMutation
|
||||
import app.omnivore.omnivore.graphql.generated.type.SaveUrlInput
|
||||
import app.omnivore.omnivore.utils.Constants
|
||||
import com.apollographql.apollo3.ApolloClient
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.TimeZone
|
||||
import java.util.UUID
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@HiltWorker
|
||||
class SaveURLWorker @AssistedInject constructor(
|
||||
@Assisted appContext: Context,
|
||||
@Assisted workerParams: WorkerParameters,
|
||||
private val datastoreRepository: DatastoreRepository,
|
||||
) : CoroutineWorker(appContext, workerParams) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
return withContext(Dispatchers.IO){
|
||||
val url = inputData.getString("url") ?: return@withContext Result.failure()
|
||||
if (saveURL(url)) Result.success() else Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveURL(url: String): Boolean {
|
||||
|
||||
val authToken = datastoreRepository.getString(omnivoreAuthToken) ?: return false
|
||||
|
||||
val apolloClient = ApolloClient.Builder()
|
||||
.serverUrl("${Constants.apiURL}/api/graphql")
|
||||
.addHttpHeader("Authorization", value = authToken)
|
||||
.build()
|
||||
|
||||
val cleanedUrl = cleanUrl(url) ?: url
|
||||
|
||||
try {
|
||||
val timezone = TimeZone.getDefault().id
|
||||
val locale = Locale.current.toLanguageTag()
|
||||
|
||||
val response = apolloClient.mutation(
|
||||
SaveUrlMutation(
|
||||
SaveUrlInput(
|
||||
clientRequestId = UUID.randomUUID().toString(),
|
||||
source = "android",
|
||||
url = cleanedUrl,
|
||||
timezone = Optional.present(timezone),
|
||||
locale = Optional.present(locale)
|
||||
)
|
||||
)
|
||||
).execute()
|
||||
|
||||
return (response.data?.saveUrl?.onSaveSuccess?.url != null)
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanUrl(text: String): String? {
|
||||
val pattern = Pattern.compile("\\b(?:https?|ftp)://\\S+")
|
||||
val matcher = pattern.matcher(text)
|
||||
|
||||
if (matcher.find()) {
|
||||
return matcher.group()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -31,6 +31,8 @@ posthog = "2.0.3"
|
||||
pspdfkit = "8.9.1"
|
||||
retrofit = "2.11.0"
|
||||
room = "2.6.1"
|
||||
workManager = "2.9.0"
|
||||
hiltWork = "1.2.0"
|
||||
|
||||
[libraries]
|
||||
accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "accompanistFlowLayout" }
|
||||
@ -81,6 +83,9 @@ room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "
|
||||
android-hilt = { group = "com.google.dagger", name = "hilt-android-gradle-plugin", version.ref = "hilt" }
|
||||
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
|
||||
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workManager"}
|
||||
hilt-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltWork"}
|
||||
hilt-work-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltWork"}
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
|
||||
|
||||
Reference in New Issue
Block a user