mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-29 16:30:31 +05:30
use download manager
This commit is contained in:
parent
6879c91dd2
commit
7159eb0472
@ -51,7 +51,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
|
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||||
@ -74,5 +74,5 @@ dependencies {
|
|||||||
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.13.1'
|
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.13.1'
|
||||||
implementation 'com.squareup.retrofit2:converter-scalars:2.1.0'
|
implementation 'com.squareup.retrofit2:converter-scalars:2.1.0'
|
||||||
|
|
||||||
implementation 'com.arthenica:ffmpeg-kit-https:4.5.1.LTS'
|
implementation 'com.arthenica:ffmpeg-kit-min:4.5.1.LTS'
|
||||||
}
|
}
|
@ -8,12 +8,30 @@
|
|||||||
"variantName": "release",
|
"variantName": "release",
|
||||||
"elements": [
|
"elements": [
|
||||||
{
|
{
|
||||||
"type": "UNIVERSAL",
|
"type": "ONE_OF_MANY",
|
||||||
"filters": [],
|
"filters": [
|
||||||
|
{
|
||||||
|
"filterType": "ABI",
|
||||||
|
"value": "x86_64"
|
||||||
|
}
|
||||||
|
],
|
||||||
"attributes": [],
|
"attributes": [],
|
||||||
"versionCode": 4,
|
"versionCode": 4,
|
||||||
"versionName": "0.2.2",
|
"versionName": "0.2.2",
|
||||||
"outputFile": "app-universal-release.apk"
|
"outputFile": "app-x86_64-release.apk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ONE_OF_MANY",
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
"filterType": "ABI",
|
||||||
|
"value": "x86"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"attributes": [],
|
||||||
|
"versionCode": 4,
|
||||||
|
"versionName": "0.2.2",
|
||||||
|
"outputFile": "app-x86-release.apk"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "ONE_OF_MANY",
|
"type": "ONE_OF_MANY",
|
||||||
@ -40,32 +58,6 @@
|
|||||||
"versionCode": 4,
|
"versionCode": 4,
|
||||||
"versionName": "0.2.2",
|
"versionName": "0.2.2",
|
||||||
"outputFile": "app-armeabi-v7a-release.apk"
|
"outputFile": "app-armeabi-v7a-release.apk"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "ONE_OF_MANY",
|
|
||||||
"filters": [
|
|
||||||
{
|
|
||||||
"filterType": "ABI",
|
|
||||||
"value": "x86_64"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"attributes": [],
|
|
||||||
"versionCode": 4,
|
|
||||||
"versionName": "0.2.2",
|
|
||||||
"outputFile": "app-x86_64-release.apk"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "ONE_OF_MANY",
|
|
||||||
"filters": [
|
|
||||||
{
|
|
||||||
"filterType": "ABI",
|
|
||||||
"value": "x86"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"attributes": [],
|
|
||||||
"versionCode": 4,
|
|
||||||
"versionName": "0.2.2",
|
|
||||||
"outputFile": "app-x86-release.apk"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"elementType": "File"
|
"elementType": "File"
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_libretube"
|
android:icon="@mipmap/ic_libretube"
|
||||||
@ -17,6 +18,7 @@
|
|||||||
android:name=".myApp"
|
android:name=".myApp"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
>
|
>
|
||||||
<activity
|
<activity
|
||||||
android:name=".Player"
|
android:name=".Player"
|
||||||
@ -37,6 +39,10 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<service
|
||||||
|
android:name=".DownloadService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
173
app/src/main/java/com/github/libretube/DownloadService.kt
Normal file
173
app/src/main/java/com/github/libretube/DownloadService.kt
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package com.github.libretube
|
||||||
|
|
||||||
|
import android.app.*
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import com.arthenica.ffmpegkit.FFmpegKit
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class DownloadService : Service(){
|
||||||
|
val TAG = "DownloadService"
|
||||||
|
private var downloadId: Long =0
|
||||||
|
private lateinit var videoId: String
|
||||||
|
private lateinit var videoUrl: String
|
||||||
|
private lateinit var audioUrl: String
|
||||||
|
private var duration: Int = 0
|
||||||
|
//private lateinit var command: String
|
||||||
|
private lateinit var audioDir: File
|
||||||
|
private lateinit var videoDir: File
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
videoId = intent?.getStringExtra("videoId")!!
|
||||||
|
videoUrl = intent.getStringExtra("videoUrl")!!
|
||||||
|
audioUrl = intent.getStringExtra("audioUrl")!!
|
||||||
|
//command = intent.getStringExtra("command")!!
|
||||||
|
duration = intent.getIntExtra("duration",1)
|
||||||
|
downloadManager()
|
||||||
|
|
||||||
|
return super.onStartCommand(intent, flags, startId)
|
||||||
|
}
|
||||||
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadManager() {
|
||||||
|
val path = applicationContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
val folder_main = ".tmp"
|
||||||
|
val f = File(path, folder_main)
|
||||||
|
if (!f.exists()) {
|
||||||
|
f.mkdirs()
|
||||||
|
Log.e(TAG, "Directory make")
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Directory already have")
|
||||||
|
}
|
||||||
|
audioDir = File(f, "$videoId-audio")
|
||||||
|
videoDir = File(f, "$videoId-video")
|
||||||
|
try {
|
||||||
|
Log.e(TAG, "Directory make")
|
||||||
|
val request: DownloadManager.Request =
|
||||||
|
DownloadManager.Request(Uri.parse(videoUrl))
|
||||||
|
.setTitle("Video") // Title of the Download Notification
|
||||||
|
.setDescription("Downloading") // Description of the Download Notification
|
||||||
|
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) // Visibility of the download Notification
|
||||||
|
.setDestinationUri(Uri.fromFile(videoDir))
|
||||||
|
.setAllowedOverMetered(true) // Set if download is allowed on Mobile network
|
||||||
|
.setAllowedOverRoaming(true) //
|
||||||
|
val downloadManager: DownloadManager =
|
||||||
|
applicationContext.getSystemService(DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
downloadId = downloadManager.enqueue(request)
|
||||||
|
|
||||||
|
registerReceiver(onDownloadComplete, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
Log.e(TAG, "download error $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
//Fetching the download id received with the broadcast
|
||||||
|
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
|
||||||
|
//Checking if the received broadcast is for our enqueued download by matching download id
|
||||||
|
if (downloadId == id) {
|
||||||
|
Toast.makeText(this@DownloadService, "Download Completed", Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
downloadId=0
|
||||||
|
val request: DownloadManager.Request =
|
||||||
|
DownloadManager.Request(Uri.parse(audioUrl))
|
||||||
|
.setTitle("Audio") // Title of the Download Notification
|
||||||
|
.setDescription("Downloading") // Description of the Download Notification
|
||||||
|
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) // Visibility of the download Notification
|
||||||
|
.setDestinationUri(Uri.fromFile(audioDir))
|
||||||
|
.setAllowedOverMetered(true) // Set if download is allowed on Mobile network
|
||||||
|
.setAllowedOverRoaming(true) //
|
||||||
|
val downloadManager: DownloadManager =
|
||||||
|
applicationContext.getSystemService(DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
downloadManager.enqueue(request)
|
||||||
|
|
||||||
|
}else if (downloadId == 0L){
|
||||||
|
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
val channelId =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val chan = NotificationChannel("service",
|
||||||
|
"DownloadService", NotificationManager.IMPORTANCE_NONE)
|
||||||
|
chan.lightColor = Color.BLUE
|
||||||
|
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||||
|
service.createNotificationChannel(chan)
|
||||||
|
"service"
|
||||||
|
} else {
|
||||||
|
// If earlier version channel ID is not used
|
||||||
|
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
//Toast.makeText(this,command, Toast.LENGTH_SHORT).show()
|
||||||
|
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||||
|
this@DownloadService, 0, intent, 0)
|
||||||
|
//Sets the maximum progress as 100
|
||||||
|
val progressMax = 100
|
||||||
|
//Creating a notification and setting its various attributes
|
||||||
|
val notification =
|
||||||
|
NotificationCompat.Builder(this@DownloadService, channelId)
|
||||||
|
.setSmallIcon(R.drawable.ic_download)
|
||||||
|
.setContentTitle("LibreTube")
|
||||||
|
.setContentText("Downloading")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setProgress(progressMax, 0, true)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
|
||||||
|
FFmpegKit.executeAsync("-y -i $videoDir -i $audioDir -c copy ${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)}/${videoId}.mkv",
|
||||||
|
{ session ->
|
||||||
|
val state = session.state
|
||||||
|
val returnCode = session.returnCode
|
||||||
|
// CALLED WHEN SESSION IS EXECUTED
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
String.format(
|
||||||
|
"FFmpeg process exited with state %s and rc %s.%s",
|
||||||
|
state,
|
||||||
|
returnCode,
|
||||||
|
session.failStackTrace
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val path = applicationContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
val folder_main = ".tmp"
|
||||||
|
val f = File(path, folder_main)
|
||||||
|
f.deleteRecursively()
|
||||||
|
stopForeground(true)
|
||||||
|
}, {
|
||||||
|
// CALLED WHEN SESSION PRINTS LOGS
|
||||||
|
Log.e(TAG,it.message.toString())
|
||||||
|
}) {
|
||||||
|
// CALLED WHEN SESSION GENERATES STATISTICS
|
||||||
|
Log.e(TAG+"stat",it.time.toString())
|
||||||
|
val progress = it.time/(10*duration!!)
|
||||||
|
if (progress<1){
|
||||||
|
notification
|
||||||
|
.setProgress(progressMax, progress.toInt(), false)
|
||||||
|
service.notify(1,notification.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startForeground(1,notification.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
unregisterReceiver(onDownloadComplete)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -3,15 +3,16 @@ package com.github.libretube
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.ActivityManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.Environment.DIRECTORY_DOWNLOADS
|
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
@ -23,13 +24,13 @@ import androidx.appcompat.app.AlertDialog
|
|||||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.arthenica.ffmpegkit.FFmpegKit
|
|
||||||
import com.github.libretube.adapters.TrendingAdapter
|
import com.github.libretube.adapters.TrendingAdapter
|
||||||
import com.github.libretube.obj.PipedStream
|
import com.github.libretube.obj.PipedStream
|
||||||
import com.github.libretube.obj.Subscribe
|
import com.github.libretube.obj.Subscribe
|
||||||
@ -48,7 +49,6 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
|
|||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
@ -364,6 +364,9 @@ class PlayerFragment : Fragment() {
|
|||||||
val subButton = view.findViewById<MaterialButton>(R.id.player_subscribe)
|
val subButton = view.findViewById<MaterialButton>(R.id.player_subscribe)
|
||||||
isSubscribed(subButton, channelId!!)
|
isSubscribed(subButton, channelId!!)
|
||||||
}
|
}
|
||||||
|
//check if livestream
|
||||||
|
if (response.duration!!>0){
|
||||||
|
//download clicked
|
||||||
relDownloadVideo.setOnClickListener {
|
relDownloadVideo.setOnClickListener {
|
||||||
val mainActivity = activity as MainActivity
|
val mainActivity = activity as MainActivity
|
||||||
Log.e(TAG,"download button clicked!")
|
Log.e(TAG,"download button clicked!")
|
||||||
@ -396,11 +399,24 @@ class PlayerFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FFmpegKit.executeAsync("-i ${response.videoStreams[0].url} -i ${response.audioStreams!![0].url} -c copy ${context?.getExternalFilesDir(DIRECTORY_DOWNLOADS)}${File.separator}output2.mkv",
|
val builderr: AlertDialog.Builder? = activity?.let {
|
||||||
|
AlertDialog.Builder(it)
|
||||||
|
}
|
||||||
|
var videos = videosNameArray.drop(1).toTypedArray()
|
||||||
|
builderr!!.setTitle(R.string.choose_quality_dialog)
|
||||||
|
.setItems(videos,
|
||||||
|
DialogInterface.OnClickListener { _, which ->
|
||||||
|
val intent = Intent(context,DownloadService::class.java)
|
||||||
|
intent.putExtra("videoId",videoId)
|
||||||
|
intent.putExtra("videoUrl",response.videoStreams[which].url)
|
||||||
|
intent.putExtra("audioUrl",response.audioStreams!![0].url)
|
||||||
|
intent.putExtra("duration",response.duration)
|
||||||
|
//intent.putExtra("command","-y -i ${response.videoStreams[which].url} -i ${response.audioStreams!![0].url} -c copy ${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)}/${videoId}.mkv")
|
||||||
|
context?.startService(intent)
|
||||||
|
/*FFmpegKit.executeAsync("-y -i ${response.videoStreams[which].url} -i ${response.audioStreams!![0].url} -c copy ${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)}/${videoId}.mkv",
|
||||||
{ session ->
|
{ session ->
|
||||||
val state = session.state
|
val state = session.state
|
||||||
val returnCode = session.returnCode
|
val returnCode = session.returnCode
|
||||||
|
|
||||||
// CALLED WHEN SESSION IS EXECUTED
|
// CALLED WHEN SESSION IS EXECUTED
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
@ -413,11 +429,17 @@ class PlayerFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
}, {
|
}, {
|
||||||
// CALLED WHEN SESSION PRINTS LOGS
|
// CALLED WHEN SESSION PRINTS LOGS
|
||||||
Log.e(TAG,it.toString())
|
Log.e(TAG,it.message.toString())
|
||||||
}) {
|
}) {
|
||||||
// CALLED WHEN SESSION GENERATES STATISTICS
|
// CALLED WHEN SESSION GENERATES STATISTICS
|
||||||
|
Log.e(TAG,it.time.toString())
|
||||||
|
}*/
|
||||||
|
})
|
||||||
|
val dialog: AlertDialog? = builderr?.create()
|
||||||
|
dialog?.show()
|
||||||
}
|
}
|
||||||
|
}else{
|
||||||
|
Toast.makeText(context,R.string.cannotDownload, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -427,8 +449,15 @@ class PlayerFragment : Fragment() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* private fun isMyServiceRunning(serviceClass: Class<*>): Boolean {
|
||||||
|
val manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager?
|
||||||
|
for (service in manager!!.getRunningServices(Int.MAX_VALUE)) {
|
||||||
|
if (serviceClass.name == service.service.className) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}*/
|
||||||
private fun isSubscribed(button: MaterialButton, channel_id: String){
|
private fun isSubscribed(button: MaterialButton, channel_id: String){
|
||||||
@SuppressLint("ResourceAsColor")
|
@SuppressLint("ResourceAsColor")
|
||||||
fun run() {
|
fun run() {
|
||||||
|
@ -23,4 +23,5 @@
|
|||||||
<string name="login_register">Login/Register</string>
|
<string name="login_register">Login/Register</string>
|
||||||
<string name="please_login">Please Login or Register from the settings to show your Subscriptions!</string>
|
<string name="please_login">Please Login or Register from the settings to show your Subscriptions!</string>
|
||||||
<string name="subscribeIsEmpty">Subscribe to some channels first!</string>
|
<string name="subscribeIsEmpty">Subscribe to some channels first!</string>
|
||||||
|
<string name="cannotDownload">Can\'t Download this stream!</string>
|
||||||
</resources>
|
</resources>
|
Loading…
x
Reference in New Issue
Block a user