Fix some layout.
This commit is contained in:
parent
9a5cd11782
commit
f79445c12c
10 changed files with 121 additions and 60 deletions
|
|
@ -16,10 +16,10 @@ class BookRepository : IBookRepository {
|
||||||
override fun getAll(): List<Book> =
|
override fun getAll(): List<Book> =
|
||||||
database.bookDao().getAll().map { it.toBook() }
|
database.bookDao().getAll().map { it.toBook() }
|
||||||
|
|
||||||
override fun getNews(): DataSource.Factory<Int, Book> =
|
override fun getNewsSource(): DataSource.Factory<Int, Book> =
|
||||||
database.bookDao().getNews().map { it.toBook() }
|
database.bookDao().getNews().map { it.toBook() }
|
||||||
|
|
||||||
override fun getBook(): DataSource.Factory<Int, Book> =
|
override fun getBooksSource(): DataSource.Factory<Int, Book> =
|
||||||
database.bookDao().getBook().map { it.toBook() }
|
database.bookDao().getBook().map { it.toBook() }
|
||||||
|
|
||||||
override suspend fun update(data: List<Book>) {
|
override suspend fun update(data: List<Book>) {
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ interface IBookRepository {
|
||||||
|
|
||||||
fun getAll(): List<Book>
|
fun getAll(): List<Book>
|
||||||
|
|
||||||
fun getNews(): DataSource.Factory<Int, Book>
|
fun getNewsSource(): DataSource.Factory<Int, Book>
|
||||||
|
|
||||||
fun getBook(): DataSource.Factory<Int, Book>
|
fun getBooksSource(): DataSource.Factory<Int, Book>
|
||||||
|
|
||||||
suspend fun update(data: List<Book>)
|
suspend fun update(data: List<Book>)
|
||||||
}
|
}
|
||||||
|
|
@ -3,10 +3,7 @@ package com.pixelized.biblib.ui.composable.items
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.Card
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.NavigateNext
|
import androidx.compose.material.icons.filled.NavigateNext
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -34,6 +31,18 @@ private val THUMBNAIL_HEIGHT: Dp = 96.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BookItem(
|
fun BookItem(
|
||||||
|
thumbnail: BookThumbnailUio?,
|
||||||
|
onClick: ((BookThumbnailUio) -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
if (thumbnail != null) {
|
||||||
|
FilledBookItem(thumbnail, onClick)
|
||||||
|
} else {
|
||||||
|
EmptyBookItem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FilledBookItem(
|
||||||
thumbnail: BookThumbnailUio,
|
thumbnail: BookThumbnailUio,
|
||||||
onClick: ((BookThumbnailUio) -> Unit)? = null,
|
onClick: ((BookThumbnailUio) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
|
|
@ -78,24 +87,12 @@ fun BookItem(
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
softWrap = false,
|
softWrap = false,
|
||||||
)
|
)
|
||||||
Row(
|
thumbnail.date?.let { date ->
|
||||||
modifier = Modifier.weight(1f),
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
verticalAlignment = Alignment.Bottom
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
|
modifier = Modifier.align(Alignment.End),
|
||||||
style = typography.caption,
|
style = typography.caption,
|
||||||
text = thumbnail.genre,
|
text = date,
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
softWrap = false,
|
|
||||||
)
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.widthIn(min = 4.dp)
|
|
||||||
.weight(1f)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
style = typography.caption,
|
|
||||||
text = thumbnail.date,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
softWrap = false,
|
softWrap = false,
|
||||||
)
|
)
|
||||||
|
|
@ -109,13 +106,61 @@ fun BookItem(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EmptyBookItem() {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
elevation = 4.dp,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.height(THUMBNAIL_HEIGHT),
|
||||||
|
) {
|
||||||
|
|
||||||
|
Placeholder(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
|
||||||
|
.clip(RoundedCornerShape(4.dp)),
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Placeholder(
|
||||||
|
modifier = Modifier.size(200.dp, 24.dp)
|
||||||
|
)
|
||||||
|
Placeholder(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(80.dp, 16.dp)
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Placeholder(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(60.dp, 14.dp)
|
||||||
|
.align(Alignment.End)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically),
|
||||||
|
imageVector = Icons.Default.NavigateNext, contentDescription = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Placeholder(modifier: Modifier) = Surface(modifier = modifier, elevation = 4.dp) {}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun BookItemLightPreview() {
|
fun BookItemLightPreview() {
|
||||||
Bob[BitmapCache::class] = BitmapCache(LocalContext.current)
|
Bob[BitmapCache::class] = BitmapCache(LocalContext.current)
|
||||||
BibLibTheme {
|
BibLibTheme {
|
||||||
val mock = BookThumbnailMock()
|
val mock = BookThumbnailMock()
|
||||||
BookItem(thumbnail = mock.bookThumbnail)
|
FilledBookItem(thumbnail = mock.bookThumbnail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,6 +170,22 @@ fun BookItemDarkPreview() {
|
||||||
Bob[BitmapCache::class] = BitmapCache(LocalContext.current)
|
Bob[BitmapCache::class] = BitmapCache(LocalContext.current)
|
||||||
BibLibTheme(darkTheme = true) {
|
BibLibTheme(darkTheme = true) {
|
||||||
val mock = BookThumbnailMock()
|
val mock = BookThumbnailMock()
|
||||||
BookItem(thumbnail = mock.bookThumbnail)
|
FilledBookItem(thumbnail = mock.bookThumbnail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun EmptyBookItemLightPreview() {
|
||||||
|
BibLibTheme {
|
||||||
|
EmptyBookItem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun EmptyBookItemDarkPreview() {
|
||||||
|
BibLibTheme(darkTheme = true) {
|
||||||
|
EmptyBookItem()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
|
@ -121,16 +122,14 @@ fun DetailPage(book: BookUio) {
|
||||||
title = stringResource(id = R.string.detail_language),
|
title = stringResource(id = R.string.detail_language),
|
||||||
label = book.language,
|
label = book.language,
|
||||||
)
|
)
|
||||||
TitleLabel(
|
book.date?.let {
|
||||||
title = stringResource(id = R.string.detail_release),
|
TitleLabel(
|
||||||
label = book.date,
|
title = stringResource(id = R.string.detail_release),
|
||||||
)
|
label = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Row(modifier = Modifier.padding(bottom = 16.dp)) {
|
Row(modifier = Modifier.padding(bottom = 16.dp)) {
|
||||||
TitleLabel(
|
|
||||||
title = stringResource(id = R.string.detail_genre),
|
|
||||||
label = book.genre,
|
|
||||||
)
|
|
||||||
TitleLabel(
|
TitleLabel(
|
||||||
title = stringResource(id = R.string.detail_series),
|
title = stringResource(id = R.string.detail_series),
|
||||||
label = book.series ?: "-",
|
label = book.series ?: "-",
|
||||||
|
|
@ -161,6 +160,7 @@ private fun RowScope.TitleLabel(
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
style = typography.body1,
|
style = typography.body1,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
text = label,
|
text = label,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ fun HomePage(
|
||||||
|
|
||||||
// https://issuetracker.google.com/issues/177245496
|
// https://issuetracker.google.com/issues/177245496
|
||||||
val data: LazyPagingItems<BookThumbnailUio> = when (currentPage) {
|
val data: LazyPagingItems<BookThumbnailUio> = when (currentPage) {
|
||||||
is Page.Home.New -> booksViewModel.books.collectAsLazyPagingItems()
|
is Page.Home.New -> booksViewModel.news.collectAsLazyPagingItems()
|
||||||
else -> booksViewModel.news.collectAsLazyPagingItems()
|
else -> booksViewModel.books.collectAsLazyPagingItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
@ -45,7 +45,7 @@ fun HomePage(
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
) {
|
) {
|
||||||
items(data) { thumbnail ->
|
items(data) { thumbnail ->
|
||||||
BookItem(thumbnail = thumbnail!!) { item ->
|
BookItem(thumbnail) { item ->
|
||||||
navigationViewModel.navigateTo(Page.Detail(item.id))
|
navigationViewModel.navigateTo(Page.Detail(item.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ data class BookThumbnailUio(
|
||||||
val genre: String,
|
val genre: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val author: String,
|
val author: String,
|
||||||
val date: String,
|
val date: String?,
|
||||||
val isNew: Boolean,
|
val isNew: Boolean,
|
||||||
) {
|
) {
|
||||||
val cover: URL = URL("${THUMBNAIL_URL}/$id.jpg")
|
val cover: URL = URL("${THUMBNAIL_URL}/$id.jpg")
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,9 @@ data class BookUio(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val title: String,
|
val title: String,
|
||||||
val author: String,
|
val author: String,
|
||||||
val genre: String,
|
|
||||||
val rating: Float,
|
val rating: Float,
|
||||||
val language: String,
|
val language: String,
|
||||||
val date: String,
|
val date: String?,
|
||||||
val series: String?,
|
val series: String?,
|
||||||
val description: String,
|
val description: String,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,5 @@ val Purple700 = Color(0xFF3700B3)
|
||||||
val Teal200 = Color(0xFF03DAC5)
|
val Teal200 = Color(0xFF03DAC5)
|
||||||
|
|
||||||
val Green600 = Color(0xFF43a047)
|
val Green600 = Color(0xFF43a047)
|
||||||
val Green600L = Color(0XFF76d275)
|
val Green600L = Color(0xFF76d275)
|
||||||
val Green600D = Color(0XFF00701a)
|
val Green600D = Color(0xFF00701a)
|
||||||
|
|
@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.PagingSource
|
||||||
import com.pixelized.biblib.model.Book
|
import com.pixelized.biblib.model.Book
|
||||||
import com.pixelized.biblib.network.client.IBibLibClient
|
import com.pixelized.biblib.network.client.IBibLibClient
|
||||||
import com.pixelized.biblib.network.factory.BookFactory
|
import com.pixelized.biblib.network.factory.BookFactory
|
||||||
|
|
@ -27,25 +28,26 @@ class BooksViewModel : ViewModel(), IBooksViewModel {
|
||||||
private val client: IBibLibClient by inject()
|
private val client: IBibLibClient by inject()
|
||||||
private val apiCache: IAPICacheRepository by inject()
|
private val apiCache: IAPICacheRepository by inject()
|
||||||
|
|
||||||
private val formatter = SimpleDateFormat("MMMM yyyy", Locale.getDefault())
|
private val formatter_long = SimpleDateFormat("MMMM yyyy", Locale.getDefault())
|
||||||
|
private val formatter_short = SimpleDateFormat("MMM yyyy", Locale.getDefault())
|
||||||
|
|
||||||
private val _state = MutableLiveData<IBooksViewModel.State>(IBooksViewModel.State.Initial)
|
private val _state = MutableLiveData<IBooksViewModel.State>(IBooksViewModel.State.Initial)
|
||||||
override val state: LiveData<IBooksViewModel.State> get() = _state
|
override val state: LiveData<IBooksViewModel.State> get() = _state
|
||||||
|
|
||||||
private val bookSource
|
private val bookSource: () -> PagingSource<Int, BookThumbnailUio>
|
||||||
get() = bookRepository.getBook()
|
get() = bookRepository.getBooksSource()
|
||||||
.map { it.toThumbnail() }
|
.map { it.toThumbnail() }
|
||||||
.asPagingSourceFactory(Dispatchers.Default)
|
.asPagingSourceFactory(Dispatchers.IO)
|
||||||
|
|
||||||
override val books: Flow<PagingData<BookThumbnailUio>> = Pager(
|
override val books: Flow<PagingData<BookThumbnailUio>> = Pager(
|
||||||
config = PagingConfig(pageSize = PAGING_SIZE),
|
config = PagingConfig(pageSize = PAGING_SIZE),
|
||||||
pagingSourceFactory = bookSource
|
pagingSourceFactory = bookSource
|
||||||
).flow
|
).flow
|
||||||
|
|
||||||
private val newsSource
|
private val newsSource: () -> PagingSource<Int, BookThumbnailUio>
|
||||||
get() = bookRepository.getNews()
|
get() = bookRepository.getNewsSource()
|
||||||
.map { it.toThumbnail() }
|
.map { it.toThumbnail() }
|
||||||
.asPagingSourceFactory(Dispatchers.Default)
|
.asPagingSourceFactory(Dispatchers.IO)
|
||||||
|
|
||||||
override val news: Flow<PagingData<BookThumbnailUio>> = Pager(
|
override val news: Flow<PagingData<BookThumbnailUio>> = Pager(
|
||||||
config = PagingConfig(pageSize = PAGING_SIZE),
|
config = PagingConfig(pageSize = PAGING_SIZE),
|
||||||
|
|
@ -107,7 +109,11 @@ class BooksViewModel : ViewModel(), IBooksViewModel {
|
||||||
genre = genre?.joinToString { it.name } ?: "",
|
genre = genre?.joinToString { it.name } ?: "",
|
||||||
title = title,
|
title = title,
|
||||||
author = author.joinToString { it.name },
|
author = author.joinToString { it.name },
|
||||||
date = formatter.format(releaseDate).capitalize(Locale.getDefault()),
|
date = if (releaseDate.time < 0) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
formatter_long.format(releaseDate).capitalize(Locale.getDefault())
|
||||||
|
},
|
||||||
isNew = isNew,
|
isNew = isNew,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -115,10 +121,13 @@ class BooksViewModel : ViewModel(), IBooksViewModel {
|
||||||
id = id,
|
id = id,
|
||||||
title = title,
|
title = title,
|
||||||
author = author.joinToString { it.name },
|
author = author.joinToString { it.name },
|
||||||
genre = genre?.joinToString { it.name } ?: "",
|
|
||||||
rating = rating?.toFloat() ?: 0.0f,
|
rating = rating?.toFloat() ?: 0.0f,
|
||||||
language = language?.displayLanguage?.capitalize(Locale.getDefault()) ?: "",
|
language = language?.displayLanguage?.capitalize(Locale.getDefault()) ?: "",
|
||||||
date = formatter.format(releaseDate).capitalize(Locale.getDefault()),
|
date = if (releaseDate.time < 0) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
formatter_short.format(releaseDate).capitalize(Locale.getDefault())
|
||||||
|
},
|
||||||
series = series?.name,
|
series = series?.name,
|
||||||
description = synopsis ?: "",
|
description = synopsis ?: "",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import com.pixelized.biblib.ui.data.BookUio
|
||||||
class BookMock {
|
class BookMock {
|
||||||
val book: BookUio = BookUio(
|
val book: BookUio = BookUio(
|
||||||
id = 90,
|
id = 90,
|
||||||
genre = "Sci-Fi",
|
|
||||||
title = "Foundation",
|
title = "Foundation",
|
||||||
author = "Asimov",
|
author = "Asimov",
|
||||||
date = "1951",
|
date = "1951",
|
||||||
|
|
@ -19,7 +18,6 @@ class BookMock {
|
||||||
112 to BookUio(
|
112 to BookUio(
|
||||||
id = 112,
|
id = 112,
|
||||||
title = "Prélude à Fondation",
|
title = "Prélude à Fondation",
|
||||||
genre = "Sci-Fi",
|
|
||||||
author = "Asimov",
|
author = "Asimov",
|
||||||
date = "1988",
|
date = "1988",
|
||||||
series = "Foundation - 1",
|
series = "Foundation - 1",
|
||||||
|
|
@ -30,7 +28,6 @@ class BookMock {
|
||||||
78 to BookUio(
|
78 to BookUio(
|
||||||
id = 78,
|
id = 78,
|
||||||
title = "L'Aube de Fondation",
|
title = "L'Aube de Fondation",
|
||||||
genre = "Sci-Fi",
|
|
||||||
author = "Asimov",
|
author = "Asimov",
|
||||||
date = "1993",
|
date = "1993",
|
||||||
series = "Foundation - 2",
|
series = "Foundation - 2",
|
||||||
|
|
@ -41,7 +38,6 @@ class BookMock {
|
||||||
90 to BookUio(
|
90 to BookUio(
|
||||||
id = 90,
|
id = 90,
|
||||||
title = "Fondation",
|
title = "Fondation",
|
||||||
genre = "Sci-Fi",
|
|
||||||
author = "Asimov",
|
author = "Asimov",
|
||||||
date = "1951",
|
date = "1951",
|
||||||
series = "Foundation - 3",
|
series = "Foundation - 3",
|
||||||
|
|
@ -52,7 +48,6 @@ class BookMock {
|
||||||
184 to BookUio(
|
184 to BookUio(
|
||||||
id = 184,
|
id = 184,
|
||||||
title = "Fondation et Empire",
|
title = "Fondation et Empire",
|
||||||
genre = "Sci-Fi",
|
|
||||||
author = "Asimov",
|
author = "Asimov",
|
||||||
date = "1952",
|
date = "1952",
|
||||||
series = "Foundation - 4",
|
series = "Foundation - 4",
|
||||||
|
|
@ -63,7 +58,6 @@ class BookMock {
|
||||||
185 to BookUio(
|
185 to BookUio(
|
||||||
id = 185,
|
id = 185,
|
||||||
title = "Seconde Fondation",
|
title = "Seconde Fondation",
|
||||||
genre = "Sci-Fi",
|
|
||||||
author = "Asimov",
|
author = "Asimov",
|
||||||
date = "1953",
|
date = "1953",
|
||||||
series = "Foundation - 5",
|
series = "Foundation - 5",
|
||||||
|
|
@ -74,7 +68,6 @@ class BookMock {
|
||||||
119 to BookUio(
|
119 to BookUio(
|
||||||
id = 119,
|
id = 119,
|
||||||
title = "Fondation foudroyée",
|
title = "Fondation foudroyée",
|
||||||
genre = "Sci-Fi",
|
|
||||||
author = "Asimov",
|
author = "Asimov",
|
||||||
date = "1982",
|
date = "1982",
|
||||||
series = "Foundation - 6",
|
series = "Foundation - 6",
|
||||||
|
|
@ -85,7 +78,6 @@ class BookMock {
|
||||||
163 to BookUio(
|
163 to BookUio(
|
||||||
id = 163,
|
id = 163,
|
||||||
title = "Terre et Fondation",
|
title = "Terre et Fondation",
|
||||||
genre = "Sci-Fi",
|
|
||||||
author = "Asimov",
|
author = "Asimov",
|
||||||
date = "1986",
|
date = "1986",
|
||||||
series = "Foundation - 7",
|
series = "Foundation - 7",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue