Feat(LibrariesScreen): Finish LibrariesScreen

Implement open source license display in LibrariesScreen
This commit is contained in:
2024-04-24 17:31:06 +08:00
parent c8f072c930
commit 32d19ae291
23 changed files with 622 additions and 10 deletions

View File

@@ -0,0 +1,26 @@
package top.fatweb.oxygen.toolbox.data.lib
import android.content.Context
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.serialization.json.Json
import top.fatweb.oxygen.toolbox.R
import top.fatweb.oxygen.toolbox.model.lib.Dependencies
import top.fatweb.oxygen.toolbox.network.Dispatcher
import top.fatweb.oxygen.toolbox.network.OxygenDispatchers
import javax.inject.Inject
class DepDataSource @Inject constructor(
private val context: Context,
@Dispatcher(OxygenDispatchers.IO) private val ioDispatcher: CoroutineDispatcher
) {
private val json = Json { ignoreUnknownKeys = true }
val dependencies = flow {
val inputStream = context.resources.openRawResource(R.raw.dependencies)
val jsonString = inputStream.bufferedReader().use { it.readText() }
val dependencies = json.decodeFromString<Dependencies>(jsonString)
emit(dependencies)
}.flowOn(ioDispatcher)
}

View File

@@ -0,0 +1,15 @@
package top.fatweb.oxygen.toolbox.di
import android.app.Application
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
fun provideContext(app: Application): Context = app.applicationContext
}

View File

@@ -8,6 +8,8 @@ import top.fatweb.oxygen.toolbox.monitor.ConnectivityManagerNetworkMonitor
import top.fatweb.oxygen.toolbox.monitor.NetworkMonitor
import top.fatweb.oxygen.toolbox.monitor.TimeZoneBroadcastMonitor
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
import top.fatweb.oxygen.toolbox.repository.lib.DepRepository
import top.fatweb.oxygen.toolbox.repository.lib.LocalDepRepository
import top.fatweb.oxygen.toolbox.repository.tool.LocalToolRepository
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
import top.fatweb.oxygen.toolbox.repository.userdata.LocalUserDataRepository
@@ -27,4 +29,7 @@ abstract class DataModule {
@Binds
internal abstract fun bindsToolRepository(toolRepository: LocalToolRepository): ToolRepository
@Binds
internal abstract fun bindsDepRepository(depRepository: LocalDepRepository): DepRepository
}

View File

@@ -0,0 +1,12 @@
package top.fatweb.oxygen.toolbox.model.lib
import kotlinx.serialization.Serializable
@Serializable
data class Dependencies(
val metadata: Metadata,
val libraries: List<Library>,
val licenses: Map<String, License>
)

View File

@@ -0,0 +1,10 @@
package top.fatweb.oxygen.toolbox.model.lib
import kotlinx.serialization.Serializable
@Serializable
data class Developer(
val name: String? = null,
val organisationUrl: String? = null
)

View File

@@ -0,0 +1,10 @@
package top.fatweb.oxygen.toolbox.model.lib
import kotlinx.serialization.Serializable
@Serializable
data class Funding(
val platform: String,
val url: String
)

View File

@@ -0,0 +1,28 @@
package top.fatweb.oxygen.toolbox.model.lib
import kotlinx.serialization.Serializable
@Serializable
data class Library(
val uniqueId: String,
val artifactVersion: String? = null,
val name: String? = null,
val description: String? = null,
val website: String? = null,
val developers: List<Developer>,
val organization: Organization? = null,
val scm: Scm? = null,
val licenses: List<String>,
val funding: List<Funding>,
val tag: String? = null
)

View File

@@ -0,0 +1,20 @@
package top.fatweb.oxygen.toolbox.model.lib
import kotlinx.serialization.Serializable
@Serializable
data class License(
val name: String,
val url: String? = null,
val year: String? = null,
val content: String? = null,
val internalHash: String? = null,
val hash: String,
val spdxId: String? = null
)

View File

@@ -0,0 +1,8 @@
package top.fatweb.oxygen.toolbox.model.lib
import kotlinx.serialization.Serializable
@Serializable
data class Metadata(
val generated: String
)

View File

@@ -0,0 +1,10 @@
package top.fatweb.oxygen.toolbox.model.lib
import kotlinx.serialization.Serializable
@Serializable
data class Organization(
val name: String,
val url: String? = null
)

View File

@@ -0,0 +1,12 @@
package top.fatweb.oxygen.toolbox.model.lib
import kotlinx.serialization.Serializable
@Serializable
data class Scm(
val connection: String? = null,
val developerConnection: String? = null,
val url: String? = null
)

View File

@@ -0,0 +1,8 @@
package top.fatweb.oxygen.toolbox.repository.lib
import kotlinx.coroutines.flow.Flow
import top.fatweb.oxygen.toolbox.model.lib.Dependencies
interface DepRepository {
val dependencies: Flow<Dependencies>
}

View File

@@ -0,0 +1,13 @@
package top.fatweb.oxygen.toolbox.repository.lib
import kotlinx.coroutines.flow.Flow
import top.fatweb.oxygen.toolbox.data.lib.DepDataSource
import top.fatweb.oxygen.toolbox.model.lib.Dependencies
import javax.inject.Inject
class LocalDepRepository @Inject constructor(
depDataSource: DepDataSource
) : DepRepository {
override val dependencies: Flow<Dependencies> =
depDataSource.dependencies
}

View File

@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -32,7 +33,7 @@ internal fun AboutRoute(
modifier: Modifier = Modifier, onBackClick: () -> Unit, onNavigateToLibraries: () -> Unit
) {
AboutScreen(
modifier = modifier,
modifier = modifier.safeDrawingPadding(),
onBackClick = onBackClick,
onNavigateToLibraries = onNavigateToLibraries
)

View File

@@ -0,0 +1,29 @@
package top.fatweb.oxygen.toolbox.ui.about
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope
import androidx.compose.foundation.lazy.staggeredgrid.items
import top.fatweb.oxygen.toolbox.ui.component.LibraryCard
fun LazyStaggeredGridScope.librariesPanel(
librariesScreenUiState: LibrariesScreenUiState,
onClickLicense: (key: String) -> Unit
) {
when (librariesScreenUiState) {
LibrariesScreenUiState.Loading -> Unit
is LibrariesScreenUiState.Success -> {
items(
items = librariesScreenUiState.dependencies.libraries,
key = { it.uniqueId }
) {
LibraryCard(
library = it,
licenses = librariesScreenUiState.dependencies.licenses.filter { entry ->
it.licenses.contains(entry.key)
},
onClickLicense = onClickLicense
)
}
}
}
}

View File

@@ -1,27 +1,71 @@
package top.fatweb.oxygen.toolbox.ui.about
import android.content.Intent
import android.net.Uri
import androidx.activity.compose.ReportDrawnWhen
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
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.safeDrawing
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import top.fatweb.oxygen.toolbox.R
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.DraggableScrollbar
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.rememberDraggableScroller
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.scrollbarState
import top.fatweb.oxygen.toolbox.ui.theme.OxygenPreviews
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
@Composable
internal fun LibrariesRoute(
modifier: Modifier = Modifier,
viewModel: LibrariesScreenViewModel = hiltViewModel(),
onBackClick: () -> Unit
) {
val librariesScreenUiState by viewModel.librariesScreenUiState.collectAsStateWithLifecycle()
LibrariesScreen(
modifier = modifier,
modifier = modifier.safeDrawingPadding(),
librariesScreenUiState = librariesScreenUiState,
onBackClick = onBackClick
)
}
@@ -29,15 +73,122 @@ internal fun LibrariesRoute(
@Composable
internal fun LibrariesScreen(
modifier: Modifier = Modifier,
librariesScreenUiState: LibrariesScreenUiState,
onBackClick: () -> Unit
) {
val configuration = LocalConfiguration.current
val context = LocalContext.current
val isLibrariesLoading = librariesScreenUiState is LibrariesScreenUiState.Loading
ReportDrawnWhen { !isLibrariesLoading }
val itemsAvailable = howManyItems(librariesScreenUiState)
val state = rememberLazyStaggeredGridState()
val scrollbarState = state.scrollbarState(itemsAvailable = itemsAvailable)
var showDialog by remember { mutableStateOf(false) }
var dialogTitle by remember { mutableStateOf("") }
var dialogContent by remember { mutableStateOf("") }
var dialogUrl by remember { mutableStateOf("") }
Column(
modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally
modifier = modifier
) {
LibrariesToolBar(
onBackClick = onBackClick
)
Spacer(modifier = Modifier.weight(1f))
Box(
modifier
.fillMaxWidth()
.weight(1f)
) {
when (librariesScreenUiState) {
LibrariesScreenUiState.Loading -> {
Text(text = stringResource(R.string.feature_settings_loading))
}
is LibrariesScreenUiState.Success -> {
val handleOnClickLicense = { key: String ->
val license = librariesScreenUiState.dependencies.licenses[key]
if (license != null) {
showDialog = true
dialogTitle = license.name
dialogContent = license.content ?: ""
dialogUrl = license.url ?: ""
}
}
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Adaptive(300.dp),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalItemSpacing = 24.dp,
state = state
) {
librariesPanel(
librariesScreenUiState = librariesScreenUiState,
onClickLicense = handleOnClickLicense
)
item(span = StaggeredGridItemSpan.FullLine) {
Spacer(modifier = Modifier.height(8.dp))
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
}
}
state.DraggableScrollbar(
modifier = Modifier
.fillMaxHeight()
.windowInsetsPadding(WindowInsets.systemBars)
.padding(horizontal = 2.dp)
.align(Alignment.CenterEnd),
state = scrollbarState, orientation = Orientation.Vertical,
onThumbMoved = state.rememberDraggableScroller(itemsAvailable = itemsAvailable)
)
}
}
}
}
if (showDialog) {
AlertDialog(
modifier = Modifier
.widthIn(max = configuration.screenWidthDp.dp - 80.dp)
.heightIn(max = configuration.screenHeightDp.dp - 40.dp),
onDismissRequest = { showDialog = false },
title = {
Text(
text = dialogTitle,
style = MaterialTheme.typography.titleLarge
)
},
text = {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
Text(
modifier = Modifier,
text = dialogContent
)
}
},
confirmButton = {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TextButton(onClick = {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(dialogUrl)))
}) {
Text(text = stringResource(R.string.core_website))
}
TextButton(onClick = { showDialog = false }) {
Text(text = stringResource(R.string.core_close))
}
}
}
)
}
}
@@ -58,4 +209,19 @@ private fun LibrariesToolBar(
)
}
}
}
}
fun howManyItems(librariesScreenUiState: LibrariesScreenUiState) =
when (librariesScreenUiState) {
LibrariesScreenUiState.Loading -> 0
is LibrariesScreenUiState.Success -> librariesScreenUiState.dependencies.libraries.size
}
@OxygenPreviews
@Composable
private fun LibrariesScreenLoadingPreview() {
OxygenTheme {
LibrariesScreen(librariesScreenUiState = LibrariesScreenUiState.Loading, onBackClick = {})
}
}

View File

@@ -0,0 +1,35 @@
package top.fatweb.oxygen.toolbox.ui.about
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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 top.fatweb.oxygen.toolbox.model.lib.Dependencies
import top.fatweb.oxygen.toolbox.repository.lib.DepRepository
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@HiltViewModel
class LibrariesScreenViewModel @Inject constructor(
depRepository: DepRepository
) : ViewModel() {
val librariesScreenUiState: StateFlow<LibrariesScreenUiState> =
depRepository.dependencies
.map {
LibrariesScreenUiState.Success(it)
}
.stateIn(
scope = viewModelScope,
initialValue = LibrariesScreenUiState.Loading,
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
)
}
sealed interface LibrariesScreenUiState {
data object Loading: LibrariesScreenUiState
data class Success(val dependencies: Dependencies) : LibrariesScreenUiState
}

File diff suppressed because one or more lines are too long