added ability to edit command template & delete. Fixed logs not scrolling. Implemented import/export templates from clipboard. Implemented Error Items Log Button
This commit is contained in:
parent
5fed145313
commit
7d5f32b4bf
30 changed files with 331 additions and 110 deletions
|
|
@ -6,6 +6,7 @@ plugins {
|
|||
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' version '2.0.1'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'kotlin-kapt'
|
||||
id "org.jetbrains.kotlin.plugin.serialization" version "1.7.21"
|
||||
}
|
||||
|
||||
def properties = new Properties()
|
||||
|
|
@ -175,5 +176,4 @@ dependencies {
|
|||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import com.deniscerri.ytdlnis.util.FileUtil
|
|||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.squareup.picasso.Picasso
|
||||
import java.io.File
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
|
@ -106,7 +107,13 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
|
|||
when(item.status){
|
||||
DownloadRepository.Status.Cancelled.toString() -> actionButton.setIconResource(R.drawable.ic_refresh)
|
||||
DownloadRepository.Status.Queued.toString() -> actionButton.setIconResource(R.drawable.ic_baseline_delete_outline_24)
|
||||
else -> actionButton.setIconResource(R.drawable.ic_baseline_file_open_24)
|
||||
else -> {
|
||||
actionButton.setIconResource(R.drawable.ic_baseline_file_open_24)
|
||||
val logFile = File(activity.filesDir.absolutePath + """/logs/${item.id} - ${item.title}##${item.type}##${item.format.format_id}.log""")
|
||||
if (!logFile.exists()){
|
||||
actionButton.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -53,14 +53,19 @@ class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activ
|
|||
// onItemClickListener.onSelected(item!!)
|
||||
// }
|
||||
|
||||
title.setOnClickListener {
|
||||
card.setOnClickListener {
|
||||
onItemClickListener.onItemClick(item!!)
|
||||
}
|
||||
|
||||
card.setOnLongClickListener {
|
||||
onItemClickListener.onDelete(item!!); true
|
||||
}
|
||||
}
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(commandTemplate: CommandTemplate)
|
||||
fun onSelected(commandTemplate: CommandTemplate)
|
||||
fun onDelete(commandTemplate: CommandTemplate)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ interface CommandTemplateDao {
|
|||
fun getAllTemplatesLiveData() : LiveData<List<CommandTemplate>>
|
||||
|
||||
@Query("SELECT * FROM templateShortcuts ORDER BY id DESC")
|
||||
fun getAllShortcuts() : LiveData<List<TemplateShortcut>>
|
||||
fun getAllShortcutsLiveData() : LiveData<List<TemplateShortcut>>
|
||||
|
||||
@Query("SELECT * FROM templateShortcuts ORDER BY id DESC")
|
||||
fun getAllShortcuts() : List<TemplateShortcut>
|
||||
|
||||
@Query("SELECT COUNT(id) FROM commandTemplates")
|
||||
fun getTotalNumber() : Int
|
||||
|
|
|
|||
|
|
@ -16,13 +16,13 @@ interface DownloadDao {
|
|||
@Query("SELECT * FROM downloads WHERE status='Queued'")
|
||||
fun getQueuedDownloads() : LiveData<List<DownloadItem>>
|
||||
|
||||
@Query("SELECT * FROM downloads WHERE status='Cancelled'")
|
||||
@Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC")
|
||||
fun getCancelledDownloads() : LiveData<List<DownloadItem>>
|
||||
|
||||
@Query("SELECT * FROM downloads WHERE status='Error'")
|
||||
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
|
||||
fun getErroredDownloads() : LiveData<List<DownloadItem>>
|
||||
|
||||
@Query("SELECT * FROM downloads WHERE status='Processing'")
|
||||
@Query("SELECT * FROM downloads WHERE status='Processing' ORDER BY id DESC")
|
||||
fun getProcessingDownloads() : LiveData<List<DownloadItem>>
|
||||
|
||||
@Query("SELECT * FROM downloads WHERE id=:id LIMIT 1")
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
package com.deniscerri.ytdlnis.database.models
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Entity(tableName = "commandTemplates")
|
||||
data class CommandTemplate(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long,
|
||||
val title: String,
|
||||
val content: String
|
||||
var title: String,
|
||||
var content: String
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.deniscerri.ytdlnis.database.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CommandTemplateExport(
|
||||
val templates: List<CommandTemplate>,
|
||||
val shortcuts: List<TemplateShortcut>
|
||||
)
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
package com.deniscerri.ytdlnis.database.models
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Entity(tableName = "templateShortcuts")
|
||||
data class TemplateShortcut(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import com.deniscerri.ytdlnis.database.models.TemplateShortcut
|
|||
|
||||
class CommandTemplateRepository(private val commandDao: CommandTemplateDao) {
|
||||
val items : LiveData<List<CommandTemplate>> = commandDao.getAllTemplatesLiveData()
|
||||
val shortcuts : LiveData<List<TemplateShortcut>> = commandDao.getAllShortcuts()
|
||||
val shortcuts : LiveData<List<TemplateShortcut>> = commandDao.getAllShortcutsLiveData()
|
||||
|
||||
fun getAll() : List<CommandTemplate> {
|
||||
return commandDao.getAllTemplates();
|
||||
|
|
@ -25,6 +25,10 @@ class CommandTemplateRepository(private val commandDao: CommandTemplateDao) {
|
|||
commandDao.delete(item.id)
|
||||
}
|
||||
|
||||
fun getAllShortCuts() : List<TemplateShortcut> {
|
||||
return commandDao.getAllShortcuts();
|
||||
}
|
||||
|
||||
suspend fun insertShortcut(item: TemplateShortcut){
|
||||
commandDao.insertShortcut(item.content)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
package com.deniscerri.ytdlnis.database.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context.CLIPBOARD_SERVICE
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import androidx.lifecycle.*
|
||||
import com.deniscerri.ytdlnis.database.DBManager
|
||||
import com.deniscerri.ytdlnis.database.models.CommandTemplate
|
||||
import com.deniscerri.ytdlnis.database.models.CommandTemplateExport
|
||||
import com.deniscerri.ytdlnis.database.models.HistoryItem
|
||||
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
|
||||
import com.deniscerri.ytdlnis.database.repository.CommandTemplateRepository
|
||||
|
|
@ -11,13 +17,20 @@ import com.deniscerri.ytdlnis.database.repository.HistoryRepository
|
|||
import com.deniscerri.ytdlnis.database.repository.HistoryRepository.HistorySort
|
||||
import com.deniscerri.ytdlnis.database.repository.HistoryRepository.HistorySortType
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonObject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class CommandTemplateViewModel(application: Application) : AndroidViewModel(application) {
|
||||
class CommandTemplateViewModel(private val application: Application) : AndroidViewModel(application) {
|
||||
private val repository: CommandTemplateRepository
|
||||
val items: LiveData<List<CommandTemplate>>
|
||||
val shortcuts : LiveData<List<TemplateShortcut>>
|
||||
private val jsonFormat = Json { prettyPrint = true }
|
||||
|
||||
init {
|
||||
val dao = DBManager.getInstance(application).commandTemplateDao
|
||||
|
|
@ -58,12 +71,53 @@ class CommandTemplateViewModel(application: Application) : AndroidViewModel(appl
|
|||
repository.update(item)
|
||||
}
|
||||
|
||||
fun importFromClipboard() = viewModelScope.launch(Dispatchers.IO) {
|
||||
suspend fun importFromClipboard() : Int {
|
||||
val allTemplates = repository.getAll()
|
||||
val allShortcuts = repository.getAllShortCuts()
|
||||
var count = 0
|
||||
val clipboard = application.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = clipboard.primaryClip!!.getItemAt(0).text.toString()
|
||||
try{
|
||||
jsonFormat.decodeFromString<CommandTemplateExport>(clip).run {
|
||||
templates.filterNot {
|
||||
allTemplates.contains(it)
|
||||
}.run {
|
||||
this.forEach {
|
||||
repository.insert(it.copy(id=0))
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
shortcuts.filterNot {
|
||||
allShortcuts.contains(it)
|
||||
}.run{
|
||||
this.forEach {
|
||||
repository.insertShortcut(it.copy(id=0))
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch (e: Exception){
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
fun exportToClipboard() {
|
||||
|
||||
fun exportToClipboard() = viewModelScope.launch(Dispatchers.IO) {
|
||||
val allTemplates = repository.getAll()
|
||||
val allShortcuts = repository.getAllShortCuts()
|
||||
val output = jsonFormat.encodeToString(
|
||||
CommandTemplateExport(
|
||||
templates = allTemplates,
|
||||
shortcuts = allShortcuts
|
||||
)
|
||||
)
|
||||
|
||||
val clipboard: ClipboardManager =
|
||||
application.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.setText(output)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -105,7 +105,14 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
|
|||
}
|
||||
|
||||
fun addSearchQueryToHistory(query: String) = viewModelScope.launch(Dispatchers.IO) {
|
||||
searchHistoryRepository.insert(query)
|
||||
val allQueries = searchHistoryRepository.getAll()
|
||||
allQueries.filterNot {
|
||||
it.query == query
|
||||
}.run {
|
||||
this.forEach {
|
||||
searchHistoryRepository.insert(it.query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAllSearchQueryHistory() = viewModelScope.launch(Dispatchers.IO){
|
||||
|
|
|
|||
|
|
@ -121,10 +121,10 @@ class ShareActivity : AppCompatActivity() {
|
|||
|
||||
private fun showDownloadSheet(it: ResultItem){
|
||||
if (sharedPreferences.getBoolean("download_card", true)){
|
||||
val bottomSheet = DownloadBottomSheetDialog(it, DownloadViewModel.Type.video)
|
||||
val bottomSheet = DownloadBottomSheetDialog(it, DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!))
|
||||
bottomSheet.show(supportFragmentManager, "downloadSingleSheet")
|
||||
}else{
|
||||
val downloadItem = downloadViewModel.createDownloadItemFromResult(it, DownloadViewModel.Type.video)
|
||||
val downloadItem = downloadViewModel.createDownloadItemFromResult(it, DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!))
|
||||
downloadViewModel.queueDownloads(listOf(downloadItem))
|
||||
this.finish()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, View.OnClickLi
|
|||
}else if (resultViewModel.itemCount.value!! == 1){
|
||||
if (sharedPreferences!!.getBoolean("download_card", true)){
|
||||
if(it.size == 1 && !firstBoot && parentFragmentManager.findFragmentByTag("downloadSingleSheet") == null){
|
||||
showSingleDownloadSheet(it[0], DownloadViewModel.Type.video)
|
||||
showSingleDownloadSheet(it[0], DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!))
|
||||
}
|
||||
}
|
||||
}else{
|
||||
|
|
|
|||
|
|
@ -109,16 +109,18 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
|
|||
|
||||
when(type) {
|
||||
Type.audio -> {
|
||||
tabLayout.selectTab(tabLayout.getTabAt(0))
|
||||
tabLayout.getTabAt(0)!!.select()
|
||||
viewPager2.setCurrentItem(0, false)
|
||||
}
|
||||
Type.video -> {
|
||||
tabLayout.selectTab(tabLayout.getTabAt(1))
|
||||
tabLayout.getTabAt(1)!!.select()
|
||||
viewPager2.setCurrentItem(1, false)
|
||||
}
|
||||
else -> {
|
||||
tabLayout.selectTab(tabLayout.getTabAt(2))
|
||||
viewPager2.setCurrentItem(2, false)
|
||||
tabLayout.getTabAt(2)!!.select()
|
||||
viewPager2.postDelayed( {
|
||||
viewPager2.setCurrentItem(2, false)
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.database.models.CommandTemplate
|
||||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
import com.deniscerri.ytdlnis.database.models.Format
|
||||
import com.deniscerri.ytdlnis.database.models.ResultItem
|
||||
|
|
@ -68,8 +69,8 @@ class DownloadCommandFragment(private val resultItem: ResultItem) : Fragment() {
|
|||
lifecycleScope.launch {
|
||||
val sharedPreferences = requireContext().getSharedPreferences("root_preferences", Activity.MODE_PRIVATE)
|
||||
try {
|
||||
val templates = withContext(Dispatchers.IO){
|
||||
commandTemplateViewModel.getAll()
|
||||
val templates : MutableList<CommandTemplate> = withContext(Dispatchers.IO){
|
||||
commandTemplateViewModel.getAll().toMutableList()
|
||||
}
|
||||
val id = sharedPreferences.getLong("commandTemplate", templates[0].id)
|
||||
val chosenCommand = templates.find { it.id == id }
|
||||
|
|
@ -117,21 +118,10 @@ class DownloadCommandFragment(private val resultItem: ResultItem) : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
val templateTitles = templates.map {it.title}
|
||||
|
||||
val commandTemplates = view.findViewById<TextInputLayout>(R.id.template)
|
||||
val autoCompleteTextView =
|
||||
view.findViewById<AutoCompleteTextView>(R.id.template_textview)
|
||||
autoCompleteTextView?.setAdapter(
|
||||
ArrayAdapter(
|
||||
requireContext(),
|
||||
android.R.layout.simple_dropdown_item_1line,
|
||||
templateTitles
|
||||
)
|
||||
)
|
||||
if (templateTitles.isNotEmpty()) {
|
||||
autoCompleteTextView!!.setText(downloadItem.format.format_id, false)
|
||||
}
|
||||
requireView().findViewById<AutoCompleteTextView>(R.id.template_textview)
|
||||
populateCommandTemplates(templates, autoCompleteTextView)
|
||||
|
||||
(commandTemplates!!.editText as AutoCompleteTextView?)!!.onItemClickListener =
|
||||
AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, index: Int, _: Long ->
|
||||
|
|
@ -168,29 +158,40 @@ class DownloadCommandFragment(private val resultItem: ResultItem) : Fragment() {
|
|||
|
||||
val newTemplate : Chip = view.findViewById(R.id.newTemplate)
|
||||
newTemplate.setOnClickListener {
|
||||
uiUtil.showCreationSheet(requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
|
||||
|
||||
uiUtil.showCommandTemplateCreationOrUpdatingSheet(null, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
|
||||
templates.add(it)
|
||||
chosenCommandView.editText!!.setText(it.content)
|
||||
downloadItem.format = Format(
|
||||
it.title,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
it.content
|
||||
)
|
||||
populateCommandTemplates(templates, autoCompleteTextView)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// val embedSubs = view.findViewById<Chip>(R.id.embed_subtitles)
|
||||
// embedSubs!!.isChecked = embedSubs.isChecked
|
||||
// embedSubs.setOnClickListener {
|
||||
// downloadItem.embedSubs = embedSubs.isChecked
|
||||
// }
|
||||
//
|
||||
// val addChapters = view.findViewById<Chip>(R.id.add_chapters)
|
||||
// addChapters!!.isChecked = addChapters.isChecked
|
||||
// addChapters.setOnClickListener{
|
||||
// downloadItem.addChapters = addChapters.isChecked
|
||||
// }
|
||||
//
|
||||
// val saveThumbnail = view.findViewById<Chip>(R.id.save_thumbnail)
|
||||
// saveThumbnail!!.isChecked = saveThumbnail.isChecked
|
||||
// saveThumbnail.setOnClickListener {
|
||||
// downloadItem.SaveThumb = saveThumbnail.isChecked
|
||||
// }
|
||||
val editSelected : Chip = view.findViewById(R.id.editSelected)
|
||||
editSelected.setOnClickListener {
|
||||
var current = templates.find { it.title == autoCompleteTextView.text.toString() }
|
||||
if (current == null) current = CommandTemplate(0, "", chosenCommandView.editText!!.text.toString())
|
||||
uiUtil.showCommandTemplateCreationOrUpdatingSheet(current, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
|
||||
templates.add(it)
|
||||
chosenCommandView.editText!!.setText(it.content)
|
||||
downloadItem.format = Format(
|
||||
it.title,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
it.content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
|
@ -198,6 +199,22 @@ class DownloadCommandFragment(private val resultItem: ResultItem) : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun populateCommandTemplates(templates: List<CommandTemplate>, autoCompleteTextView: AutoCompleteTextView?){
|
||||
val templateTitles = templates.map {it.title}
|
||||
|
||||
|
||||
autoCompleteTextView?.setAdapter(
|
||||
ArrayAdapter(
|
||||
requireContext(),
|
||||
android.R.layout.simple_dropdown_item_1line,
|
||||
templateTitles
|
||||
)
|
||||
)
|
||||
if (templateTitles.isNotEmpty()) {
|
||||
autoCompleteTextView!!.setText(downloadItem.format.format_id, false)
|
||||
}
|
||||
}
|
||||
|
||||
private var commandPathResultLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.deniscerri.ytdlnis.ui.downloadqueue
|
|||
|
||||
import android.app.Activity
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
|
|
@ -23,6 +24,7 @@ import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter
|
|||
import com.deniscerri.ytdlnis.database.models.DownloadItem
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
|
||||
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
|
||||
import com.deniscerri.ytdlnis.ui.more.downloadLogs.DownloadLogActivity
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.UiUtil
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
|
|
@ -85,7 +87,11 @@ class ErroredDownloadsFragment() : Fragment(), GenericDownloadAdapter.OnItemClic
|
|||
}
|
||||
|
||||
override fun onActionButtonClick(itemID: Long) {
|
||||
TODO("Not yet implemented")
|
||||
val item = items.find { it.id == itemID } ?: return
|
||||
val file = File(requireContext().filesDir.absolutePath + """/logs/${item.id} - ${item.title}##${item.type}##${item.format.format_id}.log""")
|
||||
val intent = Intent(requireContext(), DownloadLogActivity::class.java)
|
||||
intent.putExtra("path", file.absolutePath)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onCardClick(itemID: Long) {
|
||||
|
|
|
|||
|
|
@ -1,32 +1,27 @@
|
|||
package com.deniscerri.ytdlnis.ui.more
|
||||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.*
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.MenuItem
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.adapter.TemplatesAdapter
|
||||
import com.deniscerri.ytdlnis.database.models.CommandTemplate
|
||||
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
|
||||
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
|
||||
import com.deniscerri.ytdlnis.util.FileUtil
|
||||
import com.deniscerri.ytdlnis.util.UiUtil
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
||||
class CommandTemplatesActivity : AppCompatActivity(), TemplatesAdapter.OnItemClickListener {
|
||||
|
|
@ -68,9 +63,19 @@ class CommandTemplatesActivity : AppCompatActivity(), TemplatesAdapter.OnItemCli
|
|||
topAppBar.setOnMenuItemClickListener { m: MenuItem ->
|
||||
val itemId = m.itemId
|
||||
if (itemId == R.id.export_clipboard) {
|
||||
commandTemplateViewModel.exportToClipboard()
|
||||
lifecycleScope.launch(Dispatchers.IO){
|
||||
commandTemplateViewModel.exportToClipboard()
|
||||
}
|
||||
}else if (itemId == R.id.import_clipboard){
|
||||
commandTemplateViewModel.importFromClipboard()
|
||||
lifecycleScope.launch{
|
||||
withContext(Dispatchers.IO){
|
||||
val count = commandTemplateViewModel.importFromClipboard()
|
||||
runOnUiThread{
|
||||
Toast.makeText(this@CommandTemplatesActivity, "$count items imported!", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
|
@ -79,7 +84,7 @@ class CommandTemplatesActivity : AppCompatActivity(), TemplatesAdapter.OnItemCli
|
|||
private fun initChips() {
|
||||
val new = findViewById<Chip>(R.id.newTemplate)
|
||||
new.setOnClickListener {
|
||||
uiUtil.showCreationSheet(this@CommandTemplatesActivity, this, commandTemplateViewModel) {}
|
||||
uiUtil.showCommandTemplateCreationOrUpdatingSheet(null,this@CommandTemplatesActivity, this, commandTemplateViewModel) {}
|
||||
}
|
||||
val shortcuts = findViewById<Chip>(R.id.shortcuts)
|
||||
shortcuts.setOnClickListener {
|
||||
|
|
@ -92,10 +97,21 @@ class CommandTemplatesActivity : AppCompatActivity(), TemplatesAdapter.OnItemCli
|
|||
}
|
||||
|
||||
override fun onItemClick(commandTemplate: CommandTemplate) {
|
||||
TODO("Not yet implemented")
|
||||
uiUtil.showCommandTemplateCreationOrUpdatingSheet(commandTemplate,this@CommandTemplatesActivity, this, commandTemplateViewModel) {}
|
||||
|
||||
}
|
||||
|
||||
override fun onSelected(commandTemplate: CommandTemplate) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun onDelete(commandTemplate: CommandTemplate) {
|
||||
val deleteDialog = MaterialAlertDialogBuilder(this)
|
||||
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + commandTemplate.title + "\"!")
|
||||
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
|
||||
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
|
||||
commandTemplateViewModel.delete(commandTemplate)
|
||||
}
|
||||
deleteDialog.show()
|
||||
}
|
||||
}
|
||||
|
|
@ -36,15 +36,13 @@ class DownloadLogActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
content = findViewById(R.id.content)
|
||||
content.movementMethod = ScrollingMovementMethod()
|
||||
contentScrollView = findViewById(R.id.content_scrollview)
|
||||
|
||||
copyLog = findViewById(R.id.copy_log)
|
||||
copyLog.setOnClickListener {
|
||||
val clipboard: ClipboardManager =
|
||||
getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip: ClipData = ClipData.newPlainText("Download Log", content.text)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
clipboard.setText(content.text)
|
||||
}
|
||||
|
||||
val path = intent.getStringExtra("path")
|
||||
|
|
@ -60,8 +58,10 @@ class DownloadLogActivity : AppCompatActivity() {
|
|||
observer = object : FileObserver(file.absolutePath, MODIFY) {
|
||||
override fun onEvent(event: Int, p: String?) {
|
||||
runOnUiThread{
|
||||
content.text = File(path).readText()
|
||||
val newText = File(path).readText()
|
||||
content.text = newText
|
||||
content.scrollTo(0, content.height)
|
||||
contentScrollView.fullScroll(View.FOCUS_DOWN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -70,7 +70,8 @@ class DownloadLogActivity : AppCompatActivity() {
|
|||
observer = object : FileObserver(file, MODIFY) {
|
||||
override fun onEvent(event: Int, p: String?) {
|
||||
runOnUiThread{
|
||||
content.text = File(path).readText()
|
||||
val newText = File(path).readText()
|
||||
content.text = newText
|
||||
content.scrollTo(0, content.height)
|
||||
contentScrollView.fullScroll(View.FOCUS_DOWN)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.deniscerri.ytdlnis.ui.more.downloadLogs
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.*
|
||||
import android.view.MenuItem
|
||||
|
|
@ -13,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.deniscerri.ytdlnis.R
|
||||
import com.deniscerri.ytdlnis.adapter.DownloadLogsAdapter
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import java.io.File
|
||||
|
||||
|
||||
|
|
@ -22,6 +24,7 @@ class DownloadLogListActivity : AppCompatActivity(), DownloadLogsAdapter.OnItemC
|
|||
private lateinit var noResults: RelativeLayout
|
||||
private lateinit var fileList: MutableList<File>
|
||||
private lateinit var topAppBar: MaterialToolbar
|
||||
private lateinit var logFolder : File
|
||||
var context: Context? = null
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
@ -43,7 +46,7 @@ class DownloadLogListActivity : AppCompatActivity(), DownloadLogsAdapter.OnItemC
|
|||
noResults = findViewById(R.id.no_results)
|
||||
noResults.visibility = View.GONE
|
||||
|
||||
val logFolder = File(filesDir.absolutePath + "/logs")
|
||||
logFolder = File(filesDir.absolutePath + "/logs")
|
||||
updateList(logFolder)
|
||||
|
||||
if(Build.VERSION.SDK_INT < 29){
|
||||
|
|
@ -73,9 +76,18 @@ class DownloadLogListActivity : AppCompatActivity(), DownloadLogsAdapter.OnItemC
|
|||
val itemId = m.itemId
|
||||
if (itemId == R.id.remove_logs) {
|
||||
try{
|
||||
logFolder.listFiles()!!.forEach {
|
||||
it.delete()
|
||||
val deleteDialog = MaterialAlertDialogBuilder(this)
|
||||
deleteDialog.setTitle(getString(R.string.confirm_delete_history))
|
||||
deleteDialog.setMessage(getString(R.string.confirm_delete_logs_desc))
|
||||
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
|
||||
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
|
||||
logFolder.listFiles()!!.forEach {
|
||||
it.delete()
|
||||
}.run {
|
||||
updateList(logFolder)
|
||||
}
|
||||
}
|
||||
deleteDialog.show()
|
||||
}catch (e: Exception){
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
|
@ -113,6 +125,14 @@ class DownloadLogListActivity : AppCompatActivity(), DownloadLogsAdapter.OnItemC
|
|||
}
|
||||
|
||||
override fun onDeleteClick(file: File) {
|
||||
file.delete()
|
||||
val deleteDialog = MaterialAlertDialogBuilder(this)
|
||||
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + file.name + "\"!")
|
||||
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
|
||||
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
|
||||
file.delete().run {
|
||||
updateList(logFolder)
|
||||
}
|
||||
}
|
||||
deleteDialog.show()
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
private var videoPath: Preference? = null
|
||||
private var commandPath: Preference? = null
|
||||
private var incognito: SwitchPreferenceCompat? = null
|
||||
private var preferredDownloadType : ListPreference? = null
|
||||
private var downloadCard: SwitchPreferenceCompat? = null
|
||||
private var apiKey: EditTextPreference? = null
|
||||
private var concurrentFragments: SeekBarPreference? = null
|
||||
|
|
@ -64,6 +65,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
videoPath = findPreference("video_path")
|
||||
commandPath = findPreference("command_path")
|
||||
incognito = findPreference("incognito")
|
||||
preferredDownloadType = findPreference("preferred_download_type")
|
||||
downloadCard = findPreference("download_card")
|
||||
apiKey = findPreference("api_key")
|
||||
concurrentFragments = findPreference("concurrent_fragments")
|
||||
|
|
@ -111,6 +113,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
editor.putString("command_path", getString(R.string.command_path))
|
||||
}
|
||||
editor.putBoolean("incognito", incognito!!.isChecked)
|
||||
editor.putString("preferred_download_type", preferredDownloadType!!.value)
|
||||
editor.putBoolean("download_card", downloadCard!!.isChecked)
|
||||
editor.putString("api_key", apiKey!!.text)
|
||||
editor.putInt("concurrent_fragments", concurrentFragments!!.value)
|
||||
|
|
@ -186,6 +189,13 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
editor.apply()
|
||||
true
|
||||
}
|
||||
preferredDownloadType!!.onPreferenceChangeListener =
|
||||
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
|
||||
editor.putString("preferred_download_type", newValue.toString())
|
||||
editor.apply()
|
||||
true
|
||||
}
|
||||
|
||||
downloadCard!!.onPreferenceChangeListener =
|
||||
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
|
||||
val enable = newValue as Boolean
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import android.content.Intent
|
|||
import android.net.Uri
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
|
|
@ -18,7 +17,6 @@ import android.widget.Toast
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.deniscerri.ytdlnis.R
|
||||
|
|
@ -55,7 +53,7 @@ class UiUtil(private val fileUtil: FileUtil) {
|
|||
|
||||
}
|
||||
|
||||
fun showCreationSheet(context: Activity, lifeCycle: LifecycleOwner, commandTemplateViewModel: CommandTemplateViewModel, newTemplate: (newTemplate: CommandTemplate) -> Unit){
|
||||
fun showCommandTemplateCreationOrUpdatingSheet(item: CommandTemplate?, context: Activity, lifeCycle: LifecycleOwner, commandTemplateViewModel: CommandTemplateViewModel, newTemplate: (newTemplate: CommandTemplate) -> Unit){
|
||||
val bottomSheet = BottomSheetDialog(context)
|
||||
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
bottomSheet.setContentView(R.layout.create_command_template)
|
||||
|
|
@ -66,7 +64,15 @@ class UiUtil(private val fileUtil: FileUtil) {
|
|||
val shortcutsChipGroup : ChipGroup = bottomSheet.findViewById(R.id.shortcutsChipGroup)!!
|
||||
val editShortcuts : Button = bottomSheet.findViewById(R.id.edit_shortcuts)!!
|
||||
|
||||
ok.isEnabled = false
|
||||
if (item != null){
|
||||
title.editText!!.setText(item.title)
|
||||
content.editText!!.setText(item.content)
|
||||
bottomSheet.findViewById<TextView>(R.id.bottom_sheet_subtitle)!!.text = content.resources.getString(R.string.update_template)
|
||||
ok.text = content.resources.getString(R.string.update)
|
||||
ok.isEnabled = true
|
||||
}else{
|
||||
ok.isEnabled = false
|
||||
}
|
||||
|
||||
title.editText!!.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
|
||||
|
|
@ -91,6 +97,10 @@ class UiUtil(private val fileUtil: FileUtil) {
|
|||
}
|
||||
})
|
||||
|
||||
if (content.editText!!.text.isEmpty()){
|
||||
|
||||
}
|
||||
|
||||
content.setEndIconOnClickListener {
|
||||
if(content.editText!!.text.isEmpty()){
|
||||
val clipboard: ClipboardManager =
|
||||
|
|
@ -118,9 +128,16 @@ class UiUtil(private val fileUtil: FileUtil) {
|
|||
}
|
||||
|
||||
ok.setOnClickListener {
|
||||
val t = CommandTemplate(0, title.editText!!.text.toString(), content.editText!!.text.toString())
|
||||
commandTemplateViewModel.insert(t)
|
||||
newTemplate(t)
|
||||
if (item == null){
|
||||
val t = CommandTemplate(0, title.editText!!.text.toString(), content.editText!!.text.toString())
|
||||
commandTemplateViewModel.insert(t)
|
||||
newTemplate(t)
|
||||
}else{
|
||||
item.title = title.editText!!.text.toString()
|
||||
item.content = content.editText!!.text.toString()
|
||||
commandTemplateViewModel.update(item)
|
||||
newTemplate(item)
|
||||
}
|
||||
bottomSheet.cancel()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<vector android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?android:textColorPrimary" android:pathData="M21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19L3,19L3,5h18v14zM8,15c0,-1.66 1.34,-3 3,-3 0.35,0 0.69,0.07 1,0.18L12,6h5v2h-3v7.03c-0.02,1.64 -1.35,2.97 -3,2.97 -1.66,0 -3,-1.34 -3,-3z"/>
|
||||
<path android:fillColor="?attr/colorAccent" android:pathData="M21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19L3,19L3,5h18v14zM8,15c0,-1.66 1.34,-3 3,-3 0.35,0 0.69,0.07 1,0.18L12,6h5v2h-3v7.03c-0.02,1.64 -1.35,2.97 -3,2.97 -1.66,0 -3,-1.34 -3,-3z"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -23,27 +23,34 @@
|
|||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<ScrollView
|
||||
<LinearLayout
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:id="@+id/content_scrollview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:paddingBottom="150dp"
|
||||
>
|
||||
android:layout_height="wrap_content"
|
||||
android:descendantFocusability="blocksDescendants"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/content"
|
||||
android:padding="10dp"
|
||||
android:scrollHorizontally="true"
|
||||
android:fontFamily="monospace"
|
||||
android:textSize="15sp"
|
||||
android:layout_width="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:layout_height="wrap_content" />
|
||||
<ScrollView
|
||||
android:id="@+id/content_scrollview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="150dp"
|
||||
>
|
||||
|
||||
</ScrollView>
|
||||
<TextView
|
||||
android:id="@+id/content"
|
||||
android:padding="10dp"
|
||||
android:fontFamily="monospace"
|
||||
android:textSize="15sp"
|
||||
android:gravity="bottom"
|
||||
android:layout_width="match_parent"
|
||||
android:textIsSelectable="true"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/copy_log"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:padding="15dp">
|
||||
|
||||
<TextView
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoLink="all"
|
||||
android:text="@string/ok"
|
||||
android:text="@string/create"
|
||||
app:icon="@drawable/ic_check"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:clickable="true"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp"
|
||||
|
|
@ -17,7 +19,8 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/delete"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:focusable="true" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/delete"
|
||||
|
|
|
|||
|
|
@ -75,4 +75,17 @@
|
|||
<item>tr</item>
|
||||
<item>vi</item>
|
||||
</string-array>
|
||||
|
||||
|
||||
<string-array name="download_types">
|
||||
<item>@string/audio</item>
|
||||
<item>@string/video</item>
|
||||
<item>@string/command</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="download_types_values">
|
||||
<item>audio</item>
|
||||
<item>video</item>
|
||||
<item>command</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
|
@ -197,4 +197,10 @@
|
|||
<string name="create_shortcut">Create Shortcut</string>
|
||||
<string name="error_restarting_download">Error trying to restart the download</string>
|
||||
<string name="delete_temp_file_too">Delete temporary files from the device</string>
|
||||
<string name="update_template">Update Template</string>
|
||||
<string name="create">Create</string>
|
||||
<string name="update">Update</string>
|
||||
<string name="preferred_download_type">Preferred Download Type</string>
|
||||
<string name="preferred_download_type_summary">Default tab the download card will open to</string>
|
||||
<string name="confirm_delete_logs_desc">Delete the entire log list, permanently.</string>
|
||||
</resources>
|
||||
|
|
@ -45,6 +45,16 @@
|
|||
app:summary="@string/download_card_summary"
|
||||
app:title="@string/show_download_card" />
|
||||
|
||||
<ListPreference
|
||||
android:dependency="download_card"
|
||||
android:defaultValue="video"
|
||||
android:entries="@array/download_types"
|
||||
android:entryValues="@array/download_types_values"
|
||||
android:icon="@drawable/ic_download_type"
|
||||
app:key="preferred_download_type"
|
||||
app:summary="@string/preferred_download_type_summary"
|
||||
app:title="@string/preferred_download_type" />
|
||||
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_key"
|
||||
app:key="api_key"
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ plugins {
|
|||
id 'com.android.application' version '7.4.1' apply false
|
||||
id 'com.android.library' version '7.4.1' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.7.21' apply false
|
||||
id "org.jetbrains.kotlin.plugin.serialization" version "1.7.21" apply false
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue