added range picker on playlist

This commit is contained in:
deniscerri 2023-04-01 13:56:23 +02:00
parent 3c99f3fdc2
commit 7a8341a3c3
No known key found for this signature in database
GPG key ID: 95C43D517D830350
11 changed files with 375 additions and 164 deletions

View file

@ -76,12 +76,12 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
// CHECKBOX ----------------------------------
val check = card.findViewById<CheckBox>(R.id.checkBox)
check.isChecked = checkedItems.contains(item.url)
check.setOnCheckedChangeListener { buttonView, isChecked ->
checkCard(isChecked, item.url)
check.setOnClickListener {
checkCard(check.isChecked, item.url)
}
card.setOnClickListener {
check.isChecked = !check.isChecked
check.performClick()
}
}
@ -119,6 +119,22 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
}
}
fun checkRange(start: Int, end: Int){
checkedItems.clear()
if (start == end ){
val item = getItem(start)
checkedItems.add(item!!.url)
notifyItemChanged(start)
}else{
for (i in start..end){
val item = getItem(i)
checkedItems.add(item!!.url)
notifyItemChanged(i)
}
}
}
fun getCheckedItems() : List<String>{
return checkedItems
}

View file

@ -5,10 +5,14 @@ import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.util.DisplayMetrics
import android.util.Log
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.core.widget.doAfterTextChanged
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
@ -23,8 +27,10 @@ import com.deniscerri.ytdlnis.receiver.ShareActivity
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>, private val type: DownloadViewModel.Type) : BottomSheetDialogFragment(), PlaylistAdapter.OnItemClickListener {
@ -34,7 +40,8 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
private lateinit var recyclerView: RecyclerView
private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var selectedText: TextView
private lateinit var fromTextInput: TextInputLayout
private lateinit var toTextInput: TextInputLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -75,41 +82,90 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
selectedText = view.findViewById<Button>(R.id.selected)
selectedText.text = "0 ${resources.getString(R.string.selected)}"
fromTextInput = view.findViewById(R.id.from_textinput)
toTextInput = view.findViewById(R.id.to_textinput)
fromTextInput.editText!!.doAfterTextChanged { _text ->
reset()
val start = _text.toString()
val end = toTextInput.editText!!.text.toString()
if (checkRanges(start, end)) {
if (start < end){
var startNr = Integer.parseInt(start)
startNr--
var endNr = Integer.parseInt(end)
endNr--
if (startNr <= 0) startNr = 0
if (endNr > items.size) endNr = items.size
listAdapter.checkRange(startNr, endNr)
selectedText.text = "${listAdapter.getCheckedItems().size} ${resources.getString(R.string.selected)}"
}
}
}
toTextInput.editText!!.doAfterTextChanged { _text ->
reset()
val start = fromTextInput.editText!!.text.toString()
val end = _text.toString()
if (checkRanges(start, end)) {
if (start < end){
var startNr = Integer.parseInt(start)
startNr--
var endNr = Integer.parseInt(end)
endNr--
if (startNr <= 0) startNr = 0
if (endNr > items.size) endNr = items.size
listAdapter.checkRange(startNr, endNr)
selectedText.text = "${listAdapter.getCheckedItems().size} ${resources.getString(R.string.selected)}"
}
}
}
val checkAll = view.findViewById<FloatingActionButton>(R.id.check_all)
checkAll!!.setOnClickListener {
if (listAdapter.getCheckedItems().size != items.size){
fromTextInput.editText!!.setText("1")
toTextInput.editText!!.setText(items.size.toString())
listAdapter.checkAll()
selectedText.text = resources.getString(R.string.all_items_selected)
}else{
listAdapter.clearCheckeditems()
selectedText.text = "0 ${resources.getString(R.string.selected)}"
reset()
fromTextInput.isEnabled = true
toTextInput.isEnabled = true
fromTextInput.editText!!.setText("")
toTextInput.editText!!.setText("")
}
}
val ok = view.findViewById<Button>(R.id.bottomsheet_ok)
ok!!.setOnClickListener {
lifecycleScope.launch(Dispatchers.IO){
lifecycleScope.launch(Dispatchers.IO) {
val checkedItems = listAdapter.getCheckedItems()
val checkedResultItems = items.filter { item -> checkedItems.contains(item!!.url) }
val downloadItems = mutableListOf<DownloadItem>()
checkedResultItems.forEach { c ->
c!!.id = 0
val i = downloadViewModel.createDownloadItemFromResult(c,type)
if (type == DownloadViewModel.Type.command){
i.format = downloadViewModel.getLatestCommandTemplateAsFormat()
}
downloadItems.add(i)
}
if (downloadItems.size == 1){
val resultItem = resultViewModel.getItemByURL(items[0]!!.url)
if (checkedResultItems.size == 1){
val resultItem = resultViewModel.getItemByURL(checkedResultItems[0]!!.url)
val bottomSheet = DownloadBottomSheetDialog(resultItem, type)
bottomSheet.show(parentFragmentManager, "downloadSingleSheet")
}else{
val downloadItems = mutableListOf<DownloadItem>()
checkedResultItems.forEach { c ->
c!!.id = 0
val i = downloadViewModel.createDownloadItemFromResult(c,type)
if (type == DownloadViewModel.Type.command){
i.format = downloadViewModel.getLatestCommandTemplateAsFormat()
}
downloadItems.add(i)
}
val bottomSheet = DownloadMultipleBottomSheetDialog(downloadItems)
bottomSheet.show(parentFragmentManager, "downloadMultipleSheet")
}
dismiss()
}
@ -133,6 +189,15 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
}
}
private fun checkRanges(start: String, end: String) : Boolean {
return start.isNotBlank() && end.isNotBlank()
}
private fun reset(){
listAdapter.clearCheckeditems()
selectedText.text = "0 ${resources.getString(R.string.selected)}"
}
override fun onCardSelect(itemURL: String, isChecked: Boolean, checkedItems: List<String>) {
if (checkedItems.size == items.size){
@ -140,6 +205,13 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
}else{
selectedText.text = "${checkedItems.size} ${resources.getString(R.string.selected)}"
}
if (checkedItems.isEmpty()){
fromTextInput.isEnabled = true
toTextInput.isEnabled = true
}else{
fromTextInput.isEnabled = false
toTextInput.isEnabled = false
}
}
}

View file

@ -1,60 +1,43 @@
package com.deniscerri.ytdlnis.ui.more
import android.Manifest
import android.app.Activity
import android.app.Notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Looper
import android.text.format.DateFormat
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.*
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.work.*
import com.deniscerri.ytdlnis.App
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.work.DownloadWorker
import com.deniscerri.ytdlnis.work.TerminalDownloadWorker
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.properties.Delegates
class TerminalActivity : AppCompatActivity() {
@ -67,11 +50,15 @@ class TerminalActivity : AppCompatActivity() {
private lateinit var bottomAppBar: BottomAppBar
private lateinit var commandTemplateViewModel: CommandTemplateViewModel
private lateinit var sharedPreferences: SharedPreferences
private var downloadID by Delegates.notNull<Int>()
var context: Context? = null
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_terminal)
downloadID = System.currentTimeMillis().toInt() % 100000
context = baseContext
scrollView = findViewById(R.id.custom_command_scrollview)
topAppBar = findViewById(R.id.custom_command_toolbar)
@ -112,6 +99,28 @@ class TerminalActivity : AppCompatActivity() {
}
notificationUtil = NotificationUtil(this)
handleIntent(intent)
WorkManager.getInstance(this)
.getWorkInfosForUniqueWorkLiveData(downloadID.toString())
.observe(this){ list ->
list.forEach {work ->
if (work.state == WorkInfo.State.SUCCEEDED || work.state == WorkInfo.State.FAILED || work.state == WorkInfo.State.CANCELLED) {
input!!.visibility = View.VISIBLE
input!!.requestFocus()
hideCancelFab()
}
val line = work.progress.getString("output") ?: return@observe
val id = work.progress.getInt("id", 0)
if(id == 0) return@observe
runOnUiThread {
try {
output!!.text = "${output!!.text}\n$line"
output!!.scrollTo(0, output!!.height)
scrollView!!.fullScroll(View.FOCUS_DOWN)
}catch (ignored: Exception) {}
}
}
}
}
override fun onNewIntent(intent: Intent) {
@ -204,121 +213,31 @@ class TerminalActivity : AppCompatActivity() {
val cmd = if (command!!.contains("yt-dlp")) command.replace("yt-dlp", "")
else command
downloadID = System.currentTimeMillis().toInt()
val theIntent = Intent(this, TerminalActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, theIntent, PendingIntent.FLAG_IMMUTABLE)
val commandNotification: Notification =
notificationUtil.createDownloadServiceNotification(
pendingIntent,
getString(R.string.terminal),
downloadID,
NotificationUtil.COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID
val workRequest = OneTimeWorkRequestBuilder<TerminalDownloadWorker>()
.setInputData(
Data.Builder()
.putInt("id", downloadID)
.putString("command", cmd)
.build()
)
with(NotificationManagerCompat.from(this)){
if (ActivityCompat.checkSelfPermission(
context!!,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
notify(downloadID, commandNotification)
}
}
val request = YoutubeDLRequest(emptyList())
.addTag("terminal")
.build()
val tempFolder = StringBuilder(context!!.cacheDir.absolutePath + """/${System.currentTimeMillis()}##terminal""")
val tempFileDir = File(tempFolder.toString())
tempFileDir.delete()
tempFileDir.mkdir()
request.addOption(
"--config-locations",
File(cacheDir, "config${System.currentTimeMillis()}.txt").apply {
writeText(cmd)
}.absolutePath
)
request.addOption("-P", tempFileDir.absolutePath)
showCancelFab()
val disposable: Disposable = Observable.fromCallable {
try{
YoutubeDL.getInstance().execute(request, downloadID.toString()){ progress, _, line ->
Log.e(TAG, line)
runOnUiThread {
output!!.text = "${output!!.text}\n$line"
output!!.scrollTo(0, output!!.height)
scrollView!!.fullScroll(View.FOCUS_DOWN)
}
val title: String = getString(R.string.terminal)
notificationUtil.updateDownloadNotification(
downloadID,
line, progress.toInt(), 0, title,
NotificationUtil.COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID
)
}
}catch (e: Exception){
e.printStackTrace()
runOnUiThread {
output!!.text = "${output!!.text}\n${e.message}"
output!!.scrollTo(0, output!!.height)
scrollView!!.fullScroll(View.FOCUS_DOWN)
input!!.visibility = View.VISIBLE
input!!.requestFocus()
hideCancelFab()
}
}
}
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
input!!.setText("yt-dlp ")
input!!.visibility = View.VISIBLE
input!!.requestFocus()
try{
val fileUtil = FileUtil()
fileUtil.moveFile(tempFileDir.absoluteFile, this, sharedPreferences.getString("command_path", getString(R.string.command))!!) {}
}catch (e: Exception){
output!!.text = "${output!!.text}\n${e.message}"
output!!.scrollTo(0, output!!.height)
scrollView!!.fullScroll(View.FOCUS_DOWN)
}
hideCancelFab()
notificationUtil.cancelDownloadNotification(downloadID)
}) { e ->
e.printStackTrace()
output!!.text = "${output!!.text}\n${e.message}"
output!!.scrollTo(0, output!!.height)
scrollView!!.fullScroll(View.FOCUS_DOWN)
input!!.setText("yt-dlp ")
input!!.visibility = View.VISIBLE
hideCancelFab();
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
Log.e(DownloadWorker.TAG, context?.getString(R.string.failed_download), e)
notificationUtil.cancelDownloadNotification(downloadID)
}
compositeDisposable.add(disposable)
WorkManager.getInstance(this).beginUniqueWork(
downloadID.toString(),
ExistingWorkPolicy.KEEP,
workRequest
).enqueue()
}
private fun cancelDownload() {
YoutubeDL.getInstance().destroyProcessById(downloadID.toString())
WorkManager.getInstance(this).cancelUniqueWork(downloadID.toString())
notificationUtil.cancelDownloadNotification(downloadID)
notificationUtil.cancelDownloadNotification(downloadID.toInt())
}
companion object {
private const val TAG = "CustomCommandActivity"
private var downloadID = System.currentTimeMillis().toInt()
private val compositeDisposable = CompositeDisposable()
}

View file

@ -102,7 +102,7 @@ class DownloadLogListActivity : AppCompatActivity(), DownloadLogsAdapter.OnItemC
fileList = mutableListOf()
try{
fileList.addAll(logFolder.listFiles()!!)
fileList.sortByDescending { it.name.split(" -")[0].toInt() }
fileList.sortByDescending { it.lastModified()}
}catch (e: Exception){
e.printStackTrace()
}

View file

@ -140,6 +140,10 @@ class FileUtil() {
val titleRegex = Regex("[^A-Za-z\\d ]")
return File(context.filesDir.absolutePath + """/logs/${item.id} - ${titleRegex.replace(item.title, "")}##${item.type}##${item.format.format_id}.log""")
}
fun getLogFileForTerminal(context: Context, command: String) : File {
val titleRegex = Regex("[^A-Za-z\\d ]")
return File(context.filesDir.absolutePath + """/logs/Terminal - ${titleRegex.replace(command.take(30), "")}##terminal.log""")
}
fun convertFileSize(s: Long): String{
if (s <= 0) return "?"

View file

@ -69,7 +69,7 @@ class DownloadWorker(
val type = downloadItem.type
val downloadLocation = downloadItem.downloadPath
val tempFileDir = File(context.cacheDir.absolutePath + downloadItem.id)
val tempFileDir = File(context.cacheDir.absolutePath + "/" + downloadItem.id)
tempFileDir.delete()
tempFileDir.mkdir()

View file

@ -0,0 +1,155 @@
package com.deniscerri.ytdlnis.work
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Toast
import androidx.work.ForegroundInfo
import androidx.work.Worker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
import java.io.File
import java.util.*
class TerminalDownloadWorker(
private val context: Context,
workerParams: WorkerParameters
) : Worker(context, workerParams) {
override fun doWork(): Result {
val itemId = inputData.getInt("id", 0)
val command = inputData.getString("command")
if (itemId == 0) return Result.failure()
if (command!!.isEmpty()) return Result.failure()
val notificationUtil = NotificationUtil(context)
val fileUtil = FileUtil()
val handler = Handler(Looper.getMainLooper())
val intent = Intent(context, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, command.take(20), itemId, NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID)
val foregroundInfo = ForegroundInfo(itemId, notification)
setForegroundAsync(foregroundInfo)
val request = YoutubeDLRequest(emptyList())
val sharedPreferences = context.getSharedPreferences("root_preferences",
Service.MODE_PRIVATE
)
val downloadLocation = sharedPreferences.getString("command_path", context.getString(R.string.command_path))
val tempFileDir = File(context.cacheDir.absolutePath + "/" + itemId)
tempFileDir.delete()
tempFileDir.mkdir()
request.addOption(
"--config-locations",
File(context.cacheDir, "config${System.currentTimeMillis()}.txt").apply {
writeText(command)
}.absolutePath
)
request.addOption("-P", tempFileDir.absolutePath)
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false)
val logFolder = File(context.filesDir.absolutePath + "/logs")
val logFile = fileUtil.getLogFileForTerminal(context, command)
runCatching {
if (logDownloads){
logFolder.mkdirs()
logFile.createNewFile()
logFile.writeText("Downloading:\n" +
"Terminal Download\n" +
"Command: ${command}\n\n")
}
YoutubeDL.getInstance().execute(request, itemId.toString()){ progress, _, line ->
setProgressAsync(workDataOf("progress" to progress.toInt(), "output" to line, "id" to itemId, "log" to logDownloads))
val title: String = command.take(20)
notificationUtil.updateDownloadNotification(
itemId,
line, progress.toInt(), 0, title,
NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
)
if (logDownloads && logFile.exists()){
logFile.appendText("${line}\n")
}
}
}.onSuccess {
//move file from internal to set download directory
var finalPath : String?
try {
finalPath = moveFile(tempFileDir.absoluteFile, downloadLocation!!){ progress ->
setProgressAsync(workDataOf("progress" to progress))
}
setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $finalPath", "id" to itemId, "log" to logDownloads))
}catch (e: Exception){
finalPath = context.getString(R.string.unfound_file)
e.printStackTrace()
handler.postDelayed({
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
}, 1000)
}
//put download in history
// val incognito = sharedPreferences.getBoolean("incognito", false)
// if (!incognito) {
// val unixtime = System.currentTimeMillis() / 1000
// val file = File(finalPath!!)
// downloadItem.format.filesize = if (file.exists()) file.length() else 0L
// val historyItem = HistoryItem(0, downloadItem.url, downloadItem.title, downloadItem.author, downloadItem.duration, downloadItem.thumb, downloadItem.type, unixtime, finalPath, downloadItem.website, downloadItem.format)
// runBlocking {
// historyDao.insert(historyItem)
// }
// }
// runBlocking {
// dao.delete(downloadItem.id)
// }
}.onFailure {
if (it is YoutubeDL.CanceledException) {
return Result.failure()
}
if (logDownloads && logFile.exists()){
logFile.appendText("${it.message}\n")
}
setProgressAsync(workDataOf("progress" to -1, "output" to it.message, "id" to itemId, "log" to logDownloads))
tempFileDir.delete()
Log.e(TAG, context.getString(R.string.failed_download), it)
notificationUtil.updateDownloadNotification(
itemId,
context.getString(R.string.failed_download), 0, 0, command.take(20),
NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
)
return Result.failure()
}
return Result.success()
}
@Throws(Exception::class)
private fun moveFile(originDir: File, downLocation: String, progress: (progress: Int) -> Unit) : String{
val fileUtil = FileUtil()
val path = fileUtil.moveFile(originDir, context, downLocation){ p ->
progress(p)
}
return path
}
companion object {
const val TAG = "DownloadWorker"
}
}

View file

@ -12,4 +12,4 @@
android:fillColor="@color/icon_fg"
android:strokeWidth="1"
android:fillType="evenOdd"/>
</vector>
</vector>

View file

@ -57,11 +57,12 @@
<LinearLayout
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/cancelButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/rangeSlider">
@ -69,8 +70,8 @@
android:id="@+id/from_textinput"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="wrap_content"
android:hint="@string/start"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:hint="@string/start">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
@ -94,8 +95,8 @@
android:id="@+id/to_textinput"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="wrap_content"
android:hint="@string/end"
android:layout_height="wrap_content"
android:hint="@string/end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/colon">

View file

@ -4,17 +4,18 @@
android:id="@+id/playlist_card_constraintLayout"
android:layout_width="match_parent"
android:layout_marginBottom="10dp"
android:checkable="true"
android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:layout_height="wrap_content">
<FrameLayout
android:id="@+id/download_card_view"
android:layout_width="0dp"
android:layout_marginStart="10dp"
android:padding="10dp"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
android:layout_height="wrap_content"
android:checkable="true"
android:clickable="true"
android:focusable="true"
app:cardPreventCornerOverlap="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/checkBox"

View file

@ -77,17 +77,60 @@
android:autoLink="all"
android:text="@string/ok"
app:icon="@drawable/ic_check"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bottomsheet_ok"
app:layout_constraintVertical_bias="0.0">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/from_textinput"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
app:errorEnabled="true"
android:hint="@string/start">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/to_textinput"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="70dp"
android:layout_height="wrap_content"
android:hint="@string/end"
app:errorEnabled="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/colon">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/downloadMultipleRecyclerview"
android:layout_width="match_parent"
android:paddingTop="10dp"
tools:listitem="@layout/playlist_item"
android:layout_height="wrap_content"
android:focusableInTouchMode="true"