Add different download options within the Library (#8267)
* feat: add download options to library * feat: use max instead of min * feat: remove download all option * feat: applied requested changes + rename some functions * feat: merge downloadAllUnreadChapters and downloadUnreadChapters into one function * Apply suggestions from code review Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * feat: apply lint suggestions + fix code feat: apply lint suggestions + fix code * feat: revert onClickDownload back to onDownloadClicked Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> (cherry picked from commit 50b17d5d3416e5c44a4a98f93e9710769e74aa51) # Conflicts: # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
This commit is contained in:
parent
2f6bd9754d
commit
dd08da26e8
@ -0,0 +1,66 @@
|
|||||||
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import eu.kanade.presentation.manga.DownloadAction
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DownloadDropdownMenu(
|
||||||
|
expanded: Boolean,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onDownloadClicked: (DownloadAction) -> Unit,
|
||||||
|
includeDownloadAllOption: Boolean = true,
|
||||||
|
) {
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(text = stringResource(R.string.download_1)) },
|
||||||
|
onClick = {
|
||||||
|
onDownloadClicked(DownloadAction.NEXT_1_CHAPTER)
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(text = stringResource(R.string.download_5)) },
|
||||||
|
onClick = {
|
||||||
|
onDownloadClicked(DownloadAction.NEXT_5_CHAPTERS)
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(text = stringResource(R.string.download_10)) },
|
||||||
|
onClick = {
|
||||||
|
onDownloadClicked(DownloadAction.NEXT_10_CHAPTERS)
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(text = stringResource(R.string.download_custom)) },
|
||||||
|
onClick = {
|
||||||
|
onDownloadClicked(DownloadAction.CUSTOM)
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(text = stringResource(R.string.download_unread)) },
|
||||||
|
onClick = {
|
||||||
|
onDownloadClicked(DownloadAction.UNREAD_CHAPTERS)
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (includeDownloadAllOption) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(text = stringResource(R.string.download_all)) },
|
||||||
|
onClick = {
|
||||||
|
onDownloadClicked(DownloadAction.ALL_CHAPTERS)
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ import androidx.compose.animation.shrinkVertically
|
|||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
@ -54,6 +55,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.presentation.manga.DownloadAction
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.system.isTabletUi
|
import eu.kanade.tachiyomi.util.system.isTabletUi
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@ -218,7 +220,7 @@ fun LibraryBottomActionMenu(
|
|||||||
onChangeCategoryClicked: (() -> Unit)?,
|
onChangeCategoryClicked: (() -> Unit)?,
|
||||||
onMarkAsReadClicked: (() -> Unit)?,
|
onMarkAsReadClicked: (() -> Unit)?,
|
||||||
onMarkAsUnreadClicked: (() -> Unit)?,
|
onMarkAsUnreadClicked: (() -> Unit)?,
|
||||||
onDownloadClicked: (() -> Unit)?,
|
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||||
onDeleteClicked: (() -> Unit)?,
|
onDeleteClicked: (() -> Unit)?,
|
||||||
// SY -->
|
// SY -->
|
||||||
onClickCleanTitles: (() -> Unit)?,
|
onClickCleanTitles: (() -> Unit)?,
|
||||||
@ -270,13 +272,23 @@ fun LibraryBottomActionMenu(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (onDownloadClicked != null) {
|
if (onDownloadClicked != null) {
|
||||||
Button(
|
Box {
|
||||||
title = stringResource(R.string.action_download),
|
var downloadExpanded by remember { mutableStateOf(false) }
|
||||||
icon = Icons.Outlined.Download,
|
this@Row.Button(
|
||||||
toConfirm = confirm[3],
|
title = stringResource(R.string.action_download),
|
||||||
onLongClick = { onLongClickItem(3) },
|
icon = Icons.Outlined.Download,
|
||||||
onClick = onDownloadClicked,
|
toConfirm = confirm[3],
|
||||||
)
|
onLongClick = { onLongClickItem(3) },
|
||||||
|
onClick = { downloadExpanded = !downloadExpanded },
|
||||||
|
)
|
||||||
|
val onDismissRequest = { downloadExpanded = false }
|
||||||
|
DownloadDropdownMenu(
|
||||||
|
expanded = downloadExpanded,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onDownloadClicked = onDownloadClicked,
|
||||||
|
includeDownloadAllOption = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (onDeleteClicked != null) {
|
if (onDeleteClicked != null) {
|
||||||
Button(
|
Button(
|
||||||
|
@ -18,6 +18,7 @@ import eu.kanade.presentation.components.LoadingScreen
|
|||||||
import eu.kanade.presentation.components.Scaffold
|
import eu.kanade.presentation.components.Scaffold
|
||||||
import eu.kanade.presentation.library.components.LibraryContent
|
import eu.kanade.presentation.library.components.LibraryContent
|
||||||
import eu.kanade.presentation.library.components.LibraryToolbar
|
import eu.kanade.presentation.library.components.LibraryToolbar
|
||||||
|
import eu.kanade.presentation.manga.DownloadAction
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
|
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
|
||||||
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
|
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
|
||||||
@ -30,7 +31,7 @@ fun LibraryScreen(
|
|||||||
onChangeCategoryClicked: () -> Unit,
|
onChangeCategoryClicked: () -> Unit,
|
||||||
onMarkAsReadClicked: () -> Unit,
|
onMarkAsReadClicked: () -> Unit,
|
||||||
onMarkAsUnreadClicked: () -> Unit,
|
onMarkAsUnreadClicked: () -> Unit,
|
||||||
onDownloadClicked: () -> Unit,
|
onDownloadClicked: (DownloadAction) -> Unit,
|
||||||
onDeleteClicked: () -> Unit,
|
onDeleteClicked: () -> Unit,
|
||||||
onClickUnselectAll: () -> Unit,
|
onClickUnselectAll: () -> Unit,
|
||||||
onClickSelectAll: () -> Unit,
|
onClickSelectAll: () -> Unit,
|
||||||
|
@ -27,7 +27,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.components.AppStateBanners
|
import eu.kanade.presentation.components.AppStateBanners
|
||||||
import eu.kanade.presentation.components.DropdownMenu
|
import eu.kanade.presentation.components.DownloadDropdownMenu
|
||||||
import eu.kanade.presentation.components.OverflowMenu
|
import eu.kanade.presentation.components.OverflowMenu
|
||||||
import eu.kanade.presentation.manga.DownloadAction
|
import eu.kanade.presentation.manga.DownloadAction
|
||||||
import eu.kanade.presentation.theme.active
|
import eu.kanade.presentation.theme.active
|
||||||
@ -105,53 +105,11 @@ fun MangaToolbar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
val onDismissRequest = { onDownloadExpanded(false) }
|
val onDismissRequest = { onDownloadExpanded(false) }
|
||||||
DropdownMenu(
|
DownloadDropdownMenu(
|
||||||
expanded = downloadExpanded,
|
expanded = downloadExpanded,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
) {
|
onDownloadClicked = onClickDownload,
|
||||||
DropdownMenuItem(
|
)
|
||||||
text = { Text(text = stringResource(R.string.download_1)) },
|
|
||||||
onClick = {
|
|
||||||
onClickDownload(DownloadAction.NEXT_1_CHAPTER)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(text = stringResource(R.string.download_5)) },
|
|
||||||
onClick = {
|
|
||||||
onClickDownload(DownloadAction.NEXT_5_CHAPTERS)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(text = stringResource(R.string.download_10)) },
|
|
||||||
onClick = {
|
|
||||||
onClickDownload(DownloadAction.NEXT_10_CHAPTERS)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(text = stringResource(R.string.download_custom)) },
|
|
||||||
onClick = {
|
|
||||||
onClickDownload(DownloadAction.CUSTOM)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(text = stringResource(R.string.download_unread)) },
|
|
||||||
onClick = {
|
|
||||||
onClickDownload(DownloadAction.UNREAD_CHAPTERS)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(text = stringResource(R.string.download_all)) },
|
|
||||||
onClick = {
|
|
||||||
onClickDownload(DownloadAction.ALL_CHAPTERS)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,8 @@ import eu.kanade.domain.manga.model.toDbManga
|
|||||||
import eu.kanade.presentation.components.ChangeCategoryDialog
|
import eu.kanade.presentation.components.ChangeCategoryDialog
|
||||||
import eu.kanade.presentation.components.DeleteLibraryMangaDialog
|
import eu.kanade.presentation.components.DeleteLibraryMangaDialog
|
||||||
import eu.kanade.presentation.library.LibraryScreen
|
import eu.kanade.presentation.library.LibraryScreen
|
||||||
|
import eu.kanade.presentation.manga.DownloadAction
|
||||||
|
import eu.kanade.presentation.manga.components.DownloadCustomAmountDialog
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
||||||
@ -87,7 +89,7 @@ class LibraryController(
|
|||||||
onChangeCategoryClicked = ::showMangaCategoriesDialog,
|
onChangeCategoryClicked = ::showMangaCategoriesDialog,
|
||||||
onMarkAsReadClicked = { markReadStatus(true) },
|
onMarkAsReadClicked = { markReadStatus(true) },
|
||||||
onMarkAsUnreadClicked = { markReadStatus(false) },
|
onMarkAsUnreadClicked = { markReadStatus(false) },
|
||||||
onDownloadClicked = ::downloadUnreadChapters,
|
onDownloadClicked = ::runDownloadChapterAction,
|
||||||
onDeleteClicked = ::showDeleteMangaDialog,
|
onDeleteClicked = ::showDeleteMangaDialog,
|
||||||
onClickFilter = ::showSettingsSheet,
|
onClickFilter = ::showSettingsSheet,
|
||||||
onClickRefresh = {
|
onClickRefresh = {
|
||||||
@ -184,6 +186,16 @@ class LibraryController(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is LibraryPresenter.Dialog.DownloadCustomAmount -> {
|
||||||
|
DownloadCustomAmountDialog(
|
||||||
|
maxAmount = dialog.max,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onConfirm = { amount ->
|
||||||
|
presenter.downloadUnreadChapters(dialog.manga, amount)
|
||||||
|
presenter.clearSelection()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
null -> {}
|
null -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,9 +324,22 @@ class LibraryController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadUnreadChapters() {
|
private fun runDownloadChapterAction(action: DownloadAction) {
|
||||||
val mangaList = presenter.selection.toList()
|
val mangas = presenter.selection.map { it.manga }.toList()
|
||||||
presenter.downloadUnreadChapters(mangaList.map { it.manga })
|
when (action) {
|
||||||
|
DownloadAction.NEXT_1_CHAPTER -> presenter.downloadUnreadChapters(mangas, 1)
|
||||||
|
DownloadAction.NEXT_5_CHAPTERS -> presenter.downloadUnreadChapters(mangas, 5)
|
||||||
|
DownloadAction.NEXT_10_CHAPTERS -> presenter.downloadUnreadChapters(mangas, 10)
|
||||||
|
DownloadAction.UNREAD_CHAPTERS -> presenter.downloadUnreadChapters(mangas, null)
|
||||||
|
DownloadAction.CUSTOM -> {
|
||||||
|
presenter.dialog = LibraryPresenter.Dialog.DownloadCustomAmount(
|
||||||
|
mangas,
|
||||||
|
presenter.selection.maxOf { it.unreadCount }.toInt(),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
presenter.clearSelection()
|
presenter.clearSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +54,7 @@ import eu.kanade.tachiyomi.data.cache.CoverCache
|
|||||||
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadCache
|
import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackStatus
|
import eu.kanade.tachiyomi.data.track.TrackStatus
|
||||||
@ -64,6 +65,7 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import eu.kanade.tachiyomi.util.chapter.getChapterSort
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
@ -600,12 +602,32 @@ class LibraryPresenter(
|
|||||||
return mangaCategories.flatten().distinct().subtract(common)
|
return mangaCategories.flatten().distinct().subtract(common)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun shouldDownloadChapter(manga: Manga, chapter: Chapter): Boolean {
|
||||||
|
val activeDownload = downloadManager.queue.find { chapter.id == it.chapter.id }
|
||||||
|
val downloaded = downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source)
|
||||||
|
val state = when {
|
||||||
|
activeDownload != null -> activeDownload.status
|
||||||
|
downloaded -> Download.State.DOWNLOADED
|
||||||
|
else -> Download.State.NOT_DOWNLOADED
|
||||||
|
}
|
||||||
|
return state == Download.State.NOT_DOWNLOADED
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getNotDownloadedUnreadChapters(manga: Manga): List<Chapter> {
|
||||||
|
return getChapterByMangaId.await(manga.id)
|
||||||
|
.filter { chapter ->
|
||||||
|
!chapter.read && shouldDownloadChapter(manga, chapter)
|
||||||
|
}
|
||||||
|
.sortedWith(getChapterSort(manga, sortDescending = false))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queues all unread chapters from the given list of manga.
|
* Queues the amount specified of unread chapters from the list of mangas given.
|
||||||
*
|
*
|
||||||
* @param mangas the list of manga.
|
* @param mangas the list of manga.
|
||||||
|
* @param amount the amount to queue or null to queue all
|
||||||
*/
|
*/
|
||||||
fun downloadUnreadChapters(mangas: List<Manga>) {
|
fun downloadUnreadChapters(mangas: List<Manga>, amount: Int?) {
|
||||||
presenterScope.launchNonCancellable {
|
presenterScope.launchNonCancellable {
|
||||||
mangas.forEach { manga ->
|
mangas.forEach { manga ->
|
||||||
if (manga.source == MERGED_SOURCE_ID) {
|
if (manga.source == MERGED_SOURCE_ID) {
|
||||||
@ -614,6 +636,7 @@ class LibraryPresenter(
|
|||||||
mergedSource
|
mergedSource
|
||||||
.getChapters(manga.id)
|
.getChapters(manga.id)
|
||||||
.filter { !it.read }
|
.filter { !it.read }
|
||||||
|
.let { if (amount != null) it.take(amount) else it }
|
||||||
.groupBy { it.mangaId }
|
.groupBy { it.mangaId }
|
||||||
.forEach ab@{ (mangaId, chapters) ->
|
.forEach ab@{ (mangaId, chapters) ->
|
||||||
val mergedManga = mergedMangas.firstOrNull { it.id == mangaId } ?: return@ab
|
val mergedManga = mergedMangas.firstOrNull { it.id == mangaId } ?: return@ab
|
||||||
@ -626,8 +649,8 @@ class LibraryPresenter(
|
|||||||
?.takeUnless { it.read }
|
?.takeUnless { it.read }
|
||||||
.let(::listOfNotNull)
|
.let(::listOfNotNull)
|
||||||
} else {
|
} else {
|
||||||
/* SY <-- */ getChapterByMangaId.await(manga.id)
|
/* SY <-- */ getNotDownloadedUnreadChapters(manga)
|
||||||
.filter { !it.read }
|
.let { if (amount != null) it.take(amount) else it }
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadManager.downloadChapters(manga, chapters.map { it.toDbChapter() })
|
downloadManager.downloadChapters(manga, chapters.map { it.toDbChapter() })
|
||||||
@ -1054,6 +1077,7 @@ class LibraryPresenter(
|
|||||||
sealed class Dialog {
|
sealed class Dialog {
|
||||||
data class ChangeCategory(val manga: List<Manga>, val initialSelection: List<CheckboxState<Category>>) : Dialog()
|
data class ChangeCategory(val manga: List<Manga>, val initialSelection: List<CheckboxState<Category>>) : Dialog()
|
||||||
data class DeleteManga(val manga: List<Manga>) : Dialog()
|
data class DeleteManga(val manga: List<Manga>) : Dialog()
|
||||||
|
data class DownloadCustomAmount(val manga: List<Manga>, val max: Int) : Dialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
Loading…
x
Reference in New Issue
Block a user